# Dokumentasi Teknis & Manual Book — Peminjaman Asset Karyawan

> Dokumen ini disusun **murni dari source code lokal** (`API/` Lumen dan `MOBILE/` Flutter) per 26 September 2026 dan disinkronkan ulang pada 27 Oktober 2026 (v1.6).
> Business requirement dan kode fitur **diturunkan dari kode**. Bagian yang tidak ditemukan ditandai **⚠️ Belum ditemukan**, asumsi ditandai **⚠️ Asumsi**.
> Contoh ID/nama pada payload adalah **data sintetis**.

---

## 1. Informasi Dokumen

| Informasi | Nilai |
|---|---|
| Nama Fitur | Peminjaman Asset Karyawan (termasuk **Pengembalian** pinjaman) |
| Kode Fitur | **KRY05** (prefix nomor transaksi `KRY05-YYYYMMNNN`, dipakai untuk peminjaman dan pengembalian) |
| Modul | Asset Management |
| Backend | Laravel **Lumen ^10.0**, PHP ^8.1, JWT `tymon/jwt-auth ^2.3`, Query Builder |
| Database | Struktur mengacu `API/diagram-asset.dbml` + migrations. ⚠️ Asumsi: MySQL/MariaDB |
| Mobile | Flutter (package `professional_flutter_3_35`), `flutter_bloc`, `dio` |
| Web | ⚠️ Belum ditemukan: tidak ada source web di workspace |
| Versi Dokumen | 1.6 |
| Tanggal Dokumentasi | 2026-10-27 |
| Dokumen terkait | Onboarding, Request/Penambahan (helper & pola bersama), Offboarding (mengembalikan pinjaman saat karyawan keluar) |

---

## 2. Daftar Isi

1. Informasi Dokumen · 3. Quick Overview · 4. Ringkasan Fitur · 5. Tujuan · 6. Aktor · 7. Hak Akses · 8. Terminologi · 9. Business Rules · 10. Business Flow · 11. Flow per Role · 12. Flow Diagram · 13. Sequence Diagram · 14. Status Transaksi · 15. State Transition · 16. Struktur Database · 17. Relasi · 18. Data Dictionary · 19. Index · 20. Backend Architecture · 21. Backend Logic · 22. Dampak asset_data · 23. API · 24. Detail API · 25. Mobile Architecture · 26. Mobile Screen · 27. Mobile → API · 28. API → Mobile · 29. Mapping Mobile→API→DB · 30. Mapping DB→API→Mobile · 31. Validation · 32. Error Handling · 33. Audit Trail · 34. Notification · 35. Security · 36. Manual Book · 37. Kondisi Button · 38. Testing · 39. Temuan Ketidaksesuaian · 40. Edge Case · 41. Risk · 42. Dependency · 43. Source Reference · 44. Developer Notes · 45. Traceability · 46. DB Impact · 47. Checklist · 48. Changelog

---

## 3. Quick Overview

| Item | Informasi |
|---|---|
| Feature | Peminjaman & Pengembalian Asset Karyawan |
| Module | Asset Management |
| Actor | Karyawan (peminjam), Admin Asset (penyetuju & penerima pengembalian), System |
| Main Transaction | `asset_trx_peminjaman_karyawan` (dua jenis baris: `jenis_status = peminjaman` dan `pengembalian`) |
| Detail Transaction | `asset_trx_peminjaman_karyawan_item` |
| Relasi pengembalian | `id_peminjaman_asal` → header peminjaman |
| Asset Master | `asset_data` (`status_asset = dipinjam` selama dipinjam) |
| Log | `asset_trx_log`: `peminjaman_karyawan` (serah terima) dan `pengembalian_karyawan` (pengembalian) |
| Status | Peminjaman: `pengajuan → dikonfirmasi → aktif → selesai`, atau berhenti di `ditolak` (ditolak admin) / `dibatalkan` (dibatalkan karyawan, v1.6). Pengembalian: `pengajuan → selesai` |
| Approval | Admin memutuskan **per asset** (setujui/tolak) lalu menetapkan jadwal serah terima |
| API | 6 endpoint admin + 9 endpoint karyawan = 15 rute (jumlah rute tidak berubah di v1.6; parameter list bertambah: `tab=dibatalkan`, `tab=riwayat`, `kondisi`) |
| Mobile Screen | Karyawan: Peminjaman Asset (4 tab: Permintaan Peminjaman, Sedang Dipinjam, Pengembalian, Riwayat Peminjaman), Tambah Peminjaman Asset, Detail Peminjaman / Pengembalian. Admin: Peminjaman Asset (4 tab yang sama), Detail + sheet Konfirmasi Peminjaman Asset. Banner & kartu di Asset Saya |
| Prasyarat deploy (v1.6) | SQL manual `Claude outputs/sql-peminjaman-status-dibatalkan.sql` menambah nilai `dibatalkan` pada enum `status_peminjaman`. **Jalankan sebelum API v1.6 dipakai** (lihat RISK-010). DBML bertanda `[NEW KRY05]`; status eksekusi SQL di server tidak diverifikasi dari kode |
| Hasil tes (eksekusi sesi 2026-10-27) | API `tests/Asset`: 386 tes lulus (4850 assertion, sqlite). Mobile `test/asset`: 466 lulus, 5 gagal lama yang tidak terkait fitur (Receive action…, pemeliharaan gudang progress summary, dashboard 320px & 800px, Damaged asset replacement picker) |

```text
Karyawan: pilih asset gudang + keperluan + tujuan + tanggal pinjam–kembali → Ajukan Peminjaman
      │  asset_data → proses                                          status: pengajuan
      │   (atau Batalkan → semua asset → tersedia, status: dibatalkan)
      ▼
Admin: setujui / tolak tiap asset + jadwal serah terima (dalam periode pinjam)
      │  asset ditolak → tersedia                                     status: dikonfirmasi (atau ditolak bila semua ditolak)
      ▼
Karyawan: Terima Asset ─────────────► asset → dipinjam @karyawan + asset_trx_log     status: aktif
      │   (atau Batalkan → asset disetujui → tersedia, status: dibatalkan)
      ▼
Pengingat H-1 / terlambat (banner di aplikasi)
      ▼
Karyawan: Kembalikan Asset ─► header baru jenis pengembalian               status: pengajuan
      ▼
Admin: kondisi + lokasi gudang tiap asset ─► asset → tersedia / rusak @gudang pilihan admin + asset_trx_log
                                            pengembalian: selesai · peminjaman asal: selesai
      ▼
Tab Riwayat Peminjaman: peminjaman selesai + pengembaliannya tampil sebagai 1 entri
```

---

## 4. Ringkasan Fitur

Peminjaman Asset memungkinkan **karyawan meminjam asset gudang untuk jangka waktu tertentu** (mis. dinas, pelatihan) dan mengembalikannya. Berbeda dengan Onboarding/Penambahan yang memindahkan asset secara tetap (`terpakai`), asset pinjaman berstatus **`dipinjam`** dan wajib kembali.

Alur utama:
- **Pengajuan** oleh karyawan: memilih satu atau lebih asset yang tersedia, keperluan (Kantor/Dinas/Pelatihan/Lainnya), tujuan, tanggal pinjam dan tanggal kembali. Asset langsung dikunci.
- **Konfirmasi admin per asset**: setiap asset disetujui atau ditolak (dengan alasan). Jika ada yang disetujui, admin menentukan **jadwal serah terima** yang harus berada dalam periode pinjam.
- **Serah terima**: karyawan menekan **Terima Asset** setelah menerima fisik asset → status **Aktif**, asset tercatat dipinjam karyawan. Karyawan juga bisa **Batalkan** selama peminjaman masih `pengajuan` atau `dikonfirmasi` (sebelum menerima); hasilnya status **Dibatalkan** dan asset yang dikunci kembali tersedia.
- **Jatuh tempo**: mulai H-1 tanggal kembali, peminjaman tampil sebagai pengingat; lewat tanggal ditandai **Terlambat**.
- **Pengembalian**: karyawan mengajukan pengembalian **seluruh** asset pinjaman; admin memeriksa kondisi tiap asset (Normal → kembali tersedia, Rusak → status rusak di gudang). Peminjaman asal otomatis **Selesai**.
- **Riwayat** (v1.6): tab Riwayat Peminjaman menampilkan peminjaman `selesai` beserta pengembaliannya sebagai satu entri (jumlah normal/rusak dan tanggal dikembalikan).
- **Halaman 4 tab** (v1.6): Permintaan Peminjaman, Sedang Dipinjam (chip Semua / Jatuh Tempo / Terlambat), Pengembalian, Riwayat Peminjaman.

---

## 5. Tujuan Fitur

### 5.1 Tujuan Bisnis (diturunkan dari kode)
- Peminjaman asset tercatat dengan keperluan, tujuan, dan periode yang jelas.
- Admin mengontrol asset mana yang boleh dipinjam dan kapan diserahkan.
- Karyawan bertanggung jawab atas asset setelah menyatakan menerima secara fisik.
- Keterlambatan pengembalian terlihat oleh karyawan dan admin.
- Kondisi asset saat kembali tercatat; asset rusak tidak kembali ke stok tersedia.

### 5.2 Tujuan Sistem
- Menyimpan pengajuan, keputusan per asset, jadwal serah terima, dan pengembalian.
- Mengunci asset selama proses (`proses`) dan selama dipinjam (`dipinjam`).
- Menghitung status jatuh tempo (`sisa_hari`, `terlambat`).
- Mencatat `asset_trx_log` saat serah terima dan saat pengembalian.
- Mengirim notifikasi di setiap perubahan status.

---

## 6. Aktor

| Aktor | Role | Fungsi |
|---|---|---|
| Karyawan (peminjam) | User login (JWT) | Mengajukan peminjaman, menerima/membatalkan, mengajukan pengembalian |
| Admin Asset | `asset_admin` + karyawan aktif | Menyetujui/menolak per asset, menetapkan jadwal serah terima, memeriksa kondisi pengembalian dan memilih lokasi gudang tiap asset |
| System | Backend + FCM | Validasi, lock, penomoran, update asset, log, pengingat jatuh tempo (tampilan), notifikasi |

---

## 7. Hak Akses

| Action | Admin Asset | Karyawan | Keterangan |
|---|:---:|:---:|---|
| Melihat semua peminjaman & pengembalian | ✅ | ❌ | `/asset/peminjaman-karyawan`, `/asset/pengembalian-karyawan` |
| Melihat peminjaman & pengembalian sendiri | – | ✅ | `/asset/karyawan/peminjaman`, `/asset/karyawan/pengembalian` |
| Mengajukan peminjaman | – | ✅ | Untuk diri sendiri |
| Melihat riwayat peminjaman selesai + pengembaliannya | ✅ (semua karyawan di company) | ✅ (milik sendiri) | Tab `riwayat` dan key `pengembalian` pada detail |
| Membatalkan pengajuan (sebelum admin memutuskan) | ❌ | ✅ (status pengajuan) | Disimpan sebagai `dibatalkan` (v1.6; RISK-002 ditutup) |
| Setujui / tolak per asset | ✅ (status pengajuan) | ❌ | |
| Terima asset | ❌ | ✅ (status dikonfirmasi) | |
| Batalkan setelah disetujui | ❌ | ✅ (status dikonfirmasi) | Disimpan sebagai `dibatalkan` (v1.6; sebelumnya `ditolak`) |
| Ajukan pengembalian | ❌ | ✅ (status aktif) | Seluruh asset |
| Konfirmasi kondisi & lokasi gudang pengembalian | ✅ (status pengajuan) | ❌ | |
| Menolak pengembalian | ❌ | ❌ | ⚠️ Belum ditemukan (RISK-004) |

---

## 8. Terminologi

| Istilah | Arti di sistem |
|---|---|
| Peminjaman | Baris header `jenis_status = peminjaman` |
| Pengembalian | Baris header `jenis_status = pengembalian`, merujuk `id_peminjaman_asal` |
| Keperluan | `kantor`, `dinas`, `pelatihan`, `lainnya` |
| Tujuan peminjaman | Teks bebas (maks. 255) |
| Periode pinjam | `tgl_peminjaman` s.d. `tgl_dikembalikan` |
| Jadwal serah terima | Tanggal + jam penyerahan fisik yang ditetapkan admin, harus di dalam periode pinjam |
| Aktif | Karyawan sudah menerima asset (`status_asset = dipinjam`) |
| Jatuh tempo (banner/dashboard) | Peminjaman aktif dengan `tgl_dikembalikan` ≤ besok (termasuk yang sudah lewat), asset masih dipegang, dan belum ada pengembalian berjalan |
| Chip Jatuh Tempo (tab Sedang Dipinjam) | Parameter `kondisi=jatuh_tempo`: `tgl_dikembalikan` hari ini sampai besok; yang sudah lewat tidak termasuk (masuk chip Terlambat). Tidak mengecualikan peminjaman yang pengembaliannya sedang diajukan |
| Dibatalkan | Status `dibatalkan` (v1.6): peminjaman dibatalkan karyawan saat `pengajuan` atau `dikonfirmasi`; `ket_tolak = "Dibatalkan oleh karyawan."` |
| Riwayat | Tab `riwayat`: peminjaman `selesai` dengan ringkasan pengembaliannya (key `pengembalian`) |
| Terlambat | Aktif dan hari ini > `tgl_dikembalikan` (chip Terlambat: `kondisi=terlambat`, `tgl_dikembalikan` < hari ini) |
| `sisa_hari` | Selisih hari ke tanggal kembali; negatif = terlambat |
| `dapat_dikembalikan` | Flag detail karyawan: aktif, semua asset masih dipegang, belum ada pengembalian berjalan |

---

## 9. Business Rules

### BR-001 — Peminjaman diajukan oleh karyawan untuk dirinya sendiri
`id_karyawan` diambil dari JWT; karyawan harus aktif → "Karyawan tidak ditemukan atau tidak aktif."

### BR-002 — Data pengajuan wajib lengkap
`keperluan` (maks 255), `tujuan_peminjaman` (maks 255), `tgl_pinjam`, `tgl_kembali`, `asset_ids` (min 1, distinct) → pesan pada §24 API-009.

### BR-003 — Tanggal pinjam tidak boleh lampau; tanggal kembali ≥ tanggal pinjam
`tgl_pinjam after_or_equal:today`, `tgl_kembali after_or_equal:tgl_pinjam` → "Tanggal pinjam tidak boleh sebelum hari ini." / "Tanggal kembali tidak boleh sebelum tanggal pinjam." Tidak ada batas maksimal durasi.

### BR-004 — Asset harus tersedia di gudang
`getLockedAssets`: `status_asset = tersedia` & `pemegang_asset = gudang` (FOR UPDATE) → "Satu atau beberapa asset tidak tersedia untuk dipinjam."

### BR-005 — Asset tidak boleh sedang di onboarding atau peminjaman lain
`hasActiveOnboardingAsset` → "…sedang diproses pada onboarding." `hasActivePeminjamanAsset` (peminjaman `pengajuan`/`dikonfirmasi`/`aktif`, item tidak ditolak) → "…sedang diproses pada peminjaman lain."

### BR-006 — Asset langsung dikunci saat diajukan
`asset_data.status_asset = proses`.

### BR-007 — Admin wajib memutuskan seluruh asset
Semua item harus diberi `disetujui`/`ditolak` → "Seluruh asset peminjaman wajib dikonfirmasi." Tolak wajib alasan → "Alasan penolakan asset wajib diisi."

### BR-008 — Jadwal serah terima wajib jika ada asset disetujui dan harus dalam periode pinjam
→ "Tanggal serah terima wajib diisi." / "Jam serah terima wajib diisi." / "Tanggal serah terima harus berada dalam periode peminjaman."

### BR-009 — Hasil keputusan admin
Minimal satu disetujui → `dikonfirmasi`; semua ditolak → `ditolak` dengan `ket_tolak = "Seluruh asset peminjaman ditolak."` Asset ditolak kembali `tersedia` di gudang; asset disetujui tetap `proses`.

### BR-010 — Konfirmasi admin hanya untuk status pengajuan dan bukan milik offboarding
→ "Peminjaman asset sudah tidak menunggu konfirmasi." / "Pengembalian ini diproses melalui offboarding."

### BR-011 — Terima asset hanya saat dikonfirmasi
Karyawan aktif, status `dikonfirmasi`, asset disetujui masih `proses` di gudang → asset `dipinjam` @karyawan, status `aktif`, `asset_trx_log` → "Peminjaman asset belum dapat diterima." / "Satu atau beberapa asset belum siap diserahterimakan."

### BR-012 — Batalkan hanya saat pengajuan atau dikonfirmasi (v1.6)
Karyawan pemilik dapat membatalkan peminjaman berstatus `pengajuan` atau `dikonfirmasi`. Dari `pengajuan` seluruh asset item (belum diputuskan, `status_item` NULL) dilepas; dari `dikonfirmasi` hanya item `disetujui` (item ditolak sudah dilepas saat konfirmasi). Hanya asset yang masih `proses` + `gudang` yang diubah menjadi `tersedia` @gudang (asset lain tidak disentuh). Header menjadi **`dibatalkan`** (bukan lagi `ditolak`) dengan `ket_tolak = "Dibatalkan oleh karyawan."`. Tidak ada `asset_trx_log` karena asset tidak pernah berpindah pemegang. Status lain (`aktif`, `selesai`, `ditolak`, `dibatalkan`) → "Peminjaman asset sudah tidak dapat dibatalkan."; milik karyawan lain atau baris pengembalian → "Peminjaman asset tidak ditemukan."; karyawan nonaktif → "Karyawan tidak ditemukan atau tidak aktif."

> Data lama: pembatalan sebelum v1.6 (hanya mungkin pada `dikonfirmasi`) tersimpan sebagai `ditolak` dengan teks yang sama. SQL langkah 3 (opsional) memindahkannya ke `dibatalkan`.

### BR-013 — Pengembalian hanya untuk peminjaman aktif, seluruh asset
Semua asset disetujui harus masih `dipinjam` oleh karyawan; tidak boleh ada pengembalian berjalan; tidak sedang di offboarding → "Hanya peminjaman aktif yang dapat dikembalikan." / "Satu atau beberapa asset tidak sedang dipinjam oleh karyawan." / "Pengembalian asset sedang menunggu konfirmasi." / "Asset sedang diproses pada offboarding."

### BR-014 — Admin wajib mengisi kondisi dan lokasi gudang setiap asset yang dikembalikan
`normal`/`rusak`; rusak wajib keterangan → "Seluruh kondisi asset pengembalian wajib diisi." / "Keterangan kerusakan asset wajib diisi." (v1.1) Setiap item juga wajib punya `id_asset_lokasi_m` bertipe `gudang` pada company yang sama, tepat satu kali per item → "Lokasi gudang tujuan setiap asset wajib dipilih." / "Lokasi gudang tujuan tidak valid." / "Lokasi gudang tujuan asset tidak sesuai."

### BR-015 — Dampak pengembalian
Normal → `tersedia` @gudang; Rusak → `rusak` @gudang + `ket_rusak`; keduanya di gudang pilihan admin (`asset_data.id_asset_lokasi_m`, item `id_asset_lokasi_m_tujuan`). Pengembalian `selesai`; peminjaman asal `selesai` bila tidak ada asset pinjaman yang masih dipegang.

### BR-015a — Lokasi dikosongkan saat asset keluar gudang (v1.1)
Saat karyawan menerima asset pinjaman (Terima Asset), `asset_data.id_asset_lokasi_m` di-set `NULL` (kolom FK ke asset_lokasi_m; nilai '' ditolak database) karena asset tidak lagi berada di gudang.

### BR-016 — Pengingat jatuh tempo
Peminjaman `aktif` dengan `tgl_dikembalikan ≤ besok`, asset masih dipegang, dan belum ada pengembalian `pengajuan`/`dikonfirmasi` → tampil di Asset Saya (karyawan) dan ringkasan admin. ⚠️ Belum ditemukan: scheduler/push otomatis.

### BR-017 — Pengembalian lewat offboarding dipisahkan
Pengembalian yang dibuat oleh proses offboarding (tercatat di `asset_trx_offboarding_relasi`) disembunyikan dari tab, list, dan detail **Pengembalian** serta tidak dapat dikonfirmasi lewat menu peminjaman. **Keputusan produk (v1.6):** pengembalian dari offboarding sengaja tetap tampil di tab **Riwayat** dan pada bagian `pengembalian` di detail peminjaman, sehingga satu peminjaman selesai selalu menampilkan seluruh pengembaliannya.

### BR-019 — Filter kondisi tab Sedang Dipinjam (v1.6)
Parameter opsional `kondisi` (`semua` | `jatuh_tempo` | `terlambat`) pada list peminjaman karyawan dan admin hanya efektif bila `tab=aktif`; di tab lain diabaikan. `terlambat`: `tgl_dikembalikan` < hari ini. `jatuh_tempo`: `tgl_dikembalikan` hari ini sampai besok (yang sudah terlambat tidak masuk). `semua`/kosong = semua peminjaman aktif. Filter diterapkan di SQL **sebelum** paginasi. Nilai lain → 422 "Filter kondisi peminjaman asset tidak valid." Tanpa `tab`/`limit`/`offset` list kembali ke respons lama non-paginasi dan `kondisi` diabaikan.

### BR-020 — Tab Riwayat menggabungkan peminjaman dan pengembalian (v1.6)
`tab=riwayat` hanya berisi peminjaman `selesai`, urut `tgl_update` desc lalu id desc, paginasi sama dengan tab lain. Tiap item memuat `pengembalian[]` (hanya pengembalian `selesai` milik peminjaman dan karyawan yang sama, satu query per halaman). Detail peminjaman memuat `pengembalian[]` untuk semua status pengembalian (urut `tgl_input` asc). `riwayat` sengaja tidak ada di `filters` statis.

### BR-018 — Penomoran
ID header/item `id_company + YYYYMM + 3 digit`; `no_trx = KRY05-YYYYMMNNN` (sequence yang sama untuk peminjaman & pengembalian).

---

## 10. Business Flow

```text
1. Karyawan: Asset Saya → Peminjaman → (+) Tambah Peminjaman Asset
   → Pilih Asset (Tambah Asset) → Pilih Keperluan → Tujuan Peminjaman → Tanggal Pinjam–Kembali
   → Ajukan Peminjaman                         [pengajuan · asset proses · notif admin]
2. Admin: Asset Karyawan → Peminjaman → tab Permintaan Peminjaman (chip Pengajuan) → Detail
   → Setujui / Tolak tiap asset (+ alasan) → Konfirmasi
   → Sheet "Konfirmasi Peminjaman Asset": Jadwal Serah Terima + Jam + Catatan → Submit
                                               [dikonfirmasi / ditolak · notif karyawan]
3. Karyawan: banner / Detail → "Siap untuk meminjam Asset?"
   → Terima Asset   [aktif · asset dipinjam · asset_trx_log · notif admin]
   → Batalkan       [dibatalkan "Dibatalkan oleh karyawan." · asset tersedia · notif admin]
      (Batalkan juga tersedia sejak status pengajuan, sebelum admin memutuskan)
4. H-1 tanggal kembali → pengingat jatuh tempo; lewat tanggal → "Terlambat"
5. Karyawan: Detail peminjaman aktif → Kembalikan Asset
                                               [header pengembalian: pengajuan · notif admin]
6. Admin: Peminjaman → tab Pengembalian → Detail → Normal / Rusak tiap asset → Konfirmasi
                                               [pengembalian selesai · asset tersedia/rusak · asset_trx_log
                                                · peminjaman asal selesai · notif karyawan]
7. Riwayat: tab Riwayat Peminjaman menampilkan peminjaman selesai + ringkasan pengembaliannya
```

---

## 11. Flow Berdasarkan Role

### 11.1 Karyawan
1. **Asset Saya** → menu **Peminjaman** → halaman **Peminjaman Asset** dengan 4 tab: **Permintaan Peminjaman** (chip Pengajuan, Dikonfirmasi, Ditolak, Dibatalkan), **Sedang Dipinjam** (chip Semua, Jatuh Tempo, Terlambat), **Pengembalian** (chip Pengajuan, Selesai) dan **Riwayat Peminjaman** (tanpa chip).
2. Tekan **+** → **Tambah Peminjaman Asset** → **Ajukan Peminjaman**.
3. Selama masih **Menunggu Konfirmasi**: detail menyediakan **Batalkan**. Setelah admin mengonfirmasi: banner di Asset Saya / detail → **Terima Asset** atau **Batalkan**.
4. Selama aktif: kartu peminjaman aktif di Asset Saya, pengingat jatuh tempo, label **Terlambat** bila lewat.
5. **Kembalikan Asset** di detail peminjaman aktif → pantau di tab Pengembalian; setelah selesai peminjaman pindah ke tab Riwayat.

### 11.2 Admin Asset
1. **Asset Karyawan** → aksi **Peminjaman** (juga panel ringkasan "perlu diproses" dan "jatuh tempo").
2. Tab **Permintaan Peminjaman** → chip **Pengajuan** → detail → **Setujui**/**Tolak** per asset → **Konfirmasi** → isi jadwal → **Submit**.
3. Tab **Pengembalian** → chip Pengajuan → detail → **Normal**/**Rusak** per asset → **Konfirmasi**.
4. Tab **Sedang Dipinjam** (chip Jatuh Tempo / Terlambat) memantau peminjaman aktif; tab **Riwayat Peminjaman** (read-only) menelusuri peminjaman yang sudah selesai.

### 11.3 Approver
Dijalankan Admin Asset (§11.2 langkah 2).

### 11.4 System
Lock company, penomoran, penguncian/pelepasan asset, validasi jadwal dalam periode, perhitungan `sisa_hari`/`terlambat`/`dapat_dikembalikan`, `asset_trx_log`, penutupan peminjaman asal, notifikasi setelah commit.

---

## 12. Flow Diagram

```mermaid
flowchart TD
    A[Karyawan ajukan peminjaman] --> B{Validasi}
    B -->|Gagal| X[Pesan error]
    B -->|OK| C[status pengajuan, asset proses, notif admin]
    C --> D{Admin putuskan per asset}
    C -->|Karyawan Batalkan| H0[status dibatalkan, semua asset tersedia, notif admin]
    D -->|Semua ditolak| E[status ditolak, asset tersedia, notif karyawan]
    D -->|Ada yang disetujui + jadwal| F[status dikonfirmasi, asset ditolak tersedia, notif karyawan]
    F --> G{Karyawan}
    G -->|Batalkan| H[status dibatalkan, asset disetujui tersedia, notif admin]
    G -->|Terima Asset| I[status aktif, asset dipinjam, asset_trx_log, notif admin]
    I --> J{Tanggal kembali}
    J -->|H-1 atau lewat| K[Pengingat jatuh tempo / Terlambat]
    J --> L[Karyawan: Kembalikan Asset]
    K --> L
    L --> M[Header pengembalian status pengajuan, notif admin]
    M --> N[Admin isi kondisi dan pilih lokasi gudang tiap asset]
    N --> O[Asset tersedia atau rusak di gudang, asset_trx_log]
    O --> P[Pengembalian selesai, peminjaman asal selesai, notif karyawan]
    P --> Q[Tab Riwayat: peminjaman selesai + pengembalian]
```

---

## 13. Sequence Diagram

### 13.1 Pengajuan sampai aktif

```mermaid
sequenceDiagram
    actor K as Karyawan
    actor A as Admin
    participant M as Mobile
    participant CK as AssetKaryawanPeminjaman_c
    participant CA as AssetAdminPeminjaman_c
    participant P as AssetPeminjamanKaryawan_m
    participant DB
    participant N as AssetNotification

    K->>M: Pilih asset, keperluan, tujuan, tanggal, Ajukan
    M->>CK: POST /asset/karyawan/peminjaman
    CK->>P: store()
    P->>DB: lock company, cek karyawan, asset tersedia, tidak aktif
    P->>DB: INSERT header + item, UPDATE asset_data proses
    CK->>N: peminjaman.diajukan (admin)
    A->>M: Setujui/Tolak per asset, isi jadwal, Submit
    M->>CA: POST /asset/peminjaman-karyawan/{id}/konfirmasi
    CA->>P: konfirmasiAdmin()
    P->>DB: UPDATE item keputusan, UPDATE header dikonfirmasi + jadwal
    P->>DB: UPDATE asset_data ditolak menjadi tersedia
    CA->>N: peminjaman.disetujui (karyawan)
    K->>M: Terima Asset
    M->>CK: POST /asset/karyawan/peminjaman/{id}/terima
    CK->>P: terimaKaryawan()
    P->>DB: UPDATE asset_data dipinjam @karyawan, header aktif
    P->>DB: INSERT asset_trx_log peminjaman_karyawan
    CK->>N: peminjaman.diterima (admin)
```

### 13.2 Pengembalian

```mermaid
sequenceDiagram
    actor K as Karyawan
    actor A as Admin
    participant M as Mobile
    participant CK as AssetKaryawanPeminjaman_c
    participant CA as AssetAdminPengembalian_c
    participant P as AssetPeminjamanKaryawan_m
    participant DB
    participant N as AssetNotification

    K->>M: Kembalikan Asset
    M->>CK: POST /asset/karyawan/peminjaman/{id}/pengembalian
    CK->>P: ajukanPengembalianKaryawan()
    P->>DB: cek aktif, asset masih dipinjam, tidak ada pengembalian/offboarding
    P->>DB: INSERT header pengembalian (id_peminjaman_asal) + item
    CK->>N: pengembalian.diajukan (admin)
    A->>M: Isi Normal/Rusak per asset, Konfirmasi
    M->>CA: POST /asset/pengembalian-karyawan/{id}/konfirmasi
    CA->>P: konfirmasiPengembalianAdmin()
    P->>DB: UPDATE item, UPDATE asset_data tersedia atau rusak @gudang, id_asset_lokasi_m
    P->>DB: INSERT asset_trx_log pengembalian_karyawan
    P->>DB: UPDATE pengembalian selesai, peminjaman asal selesai
    CA->>N: pengembalian.dikonfirmasi (karyawan)
```

### 13.3 Pembatalan oleh karyawan (v1.6)

```mermaid
sequenceDiagram
    actor K as Karyawan
    participant M as Mobile
    participant CK as AssetKaryawanPeminjaman_c
    participant P as AssetPeminjamanKaryawan_m
    participant DB
    participant N as AssetNotification

    K->>M: Batalkan (status pengajuan atau dikonfirmasi)
    M->>CK: POST /asset/karyawan/peminjaman/{id}/batalkan
    CK->>P: batalkanKaryawan()
    P->>DB: lock company, header milik karyawan FOR UPDATE
    P->>DB: cek status pengajuan atau dikonfirmasi, karyawan aktif
    P->>DB: UPDATE asset_data proses + gudang menjadi tersedia (item pengajuan: semua, dikonfirmasi: disetujui)
    P->>DB: UPDATE header dibatalkan, ket_tolak
    CK->>N: peminjaman.dibatalkan (admin)
```

---

## 14. Status Transaksi

Kolom `status` (enum `status_peminjaman`: `pengajuan`, `dikonfirmasi`, `ditolak`, `aktif`, `selesai`, **`dibatalkan`** (v1.6, via SQL manual); default `pengajuan`). Di API dikirim sebagai `status_approval`. Enum ini dipakai bersama baris pengembalian, tetapi kode hanya memakai `dibatalkan` untuk peminjaman.

### 14.1 Peminjaman (`jenis_status = peminjaman`)
| Status | Label API | Tab | Arti | Aksi berikutnya |
|---|---|---|---|---|
| `pengajuan` | Menunggu Konfirmasi | pengajuan | Menunggu keputusan admin | Admin: Konfirmasi; Karyawan: Batalkan |
| `dikonfirmasi` | Dikonfirmasi | dikonfirmasi | Minimal satu asset disetujui, jadwal ditetapkan | Karyawan: Terima / Batalkan |
| `ditolak` | Ditolak | ditolak | Semua asset ditolak admin. Data sebelum v1.6 juga memuat pembatalan karyawan (`ket_tolak = "Dibatalkan oleh karyawan."`) | – |
| `dibatalkan` | Dibatalkan | dibatalkan | Dibatalkan karyawan saat `pengajuan` atau `dikonfirmasi` (v1.6) | – |
| `aktif` | Aktif (mobile: **Terlambat** bila lewat tanggal kembali) | aktif | Asset dipegang karyawan | Karyawan: Kembalikan Asset |
| `selesai` | Selesai | selesai (list: tab `riwayat`) | Seluruh asset sudah kembali | – (tidak tampil di tab Permintaan/Sedang Dipinjam; tampil di tab Riwayat) |

### 14.2 Pengembalian (`jenis_status = pengembalian`)
| Status | Label API | Tab | Arti |
|---|---|---|---|
| `pengajuan` | Menunggu Konfirmasi | pengajuan | Menunggu pemeriksaan admin |
| `dikonfirmasi` | Dikonfirmasi | pengajuan | Hanya dipakai pengembalian yang dibuat **offboarding** |
| `ditolak` | Ditolak | selesai | ⚠️ Tidak pernah di-set |
| `selesai` | Selesai | selesai | Kondisi dicatat, asset kembali ke gudang |

### 14.3 Status item (`status_item`, enum `approval_item`)
| Konteks | Nilai DB | Nilai API | Arti |
|---|---|---|---|
| Peminjaman | NULL | `''` | Belum diputuskan |
| Peminjaman | `disetujui` / `ditolak` | sama | Keputusan admin (+`ket_tolak`) |
| Pengembalian | `disetujui` | `normal` | Kondisi normal |
| Pengembalian | `ditolak` | `rusak` | Kondisi rusak (`ket_tolak` = keterangan kerusakan) |

Migrasi `2026_09_17_000009`: nilai lama 1=pengajuan, 2=dikonfirmasi, 3=ditolak, 4=aktif (peminjaman) / selesai (pengembalian).

---

## 15. State Transition

```mermaid
stateDiagram-v2
    [*] --> pengajuan: Ajukan Peminjaman (Karyawan)
    pengajuan --> dikonfirmasi: Konfirmasi, ada asset disetujui (Admin)
    pengajuan --> ditolak: Konfirmasi, semua ditolak (Admin)
    dikonfirmasi --> aktif: Terima Asset (Karyawan)
    pengajuan --> dibatalkan: Batalkan (Karyawan)
    dikonfirmasi --> dibatalkan: Batalkan (Karyawan)
    aktif --> selesai: Pengembalian dikonfirmasi (Admin)
    ditolak --> [*]
    dibatalkan --> [*]
    selesai --> [*]
```

| Status Awal | Action | Status Akhir | Actor |
|---|---|---|---|
| – | Ajukan Peminjaman | pengajuan | Karyawan |
| pengajuan | Konfirmasi (≥1 disetujui) | dikonfirmasi | Admin |
| pengajuan | Konfirmasi (semua ditolak) | ditolak | Admin |
| dikonfirmasi | Terima Asset | aktif | Karyawan |
| pengajuan | Batalkan | dibatalkan | Karyawan |
| dikonfirmasi | Batalkan | dibatalkan | Karyawan |
| aktif | Ajukan pengembalian | aktif (+ header pengembalian `pengajuan`) | Karyawan |
| aktif | Pengembalian dikonfirmasi | selesai | Admin (otomatis) |
| pengajuan (pengembalian) | Konfirmasi kondisi | selesai | Admin |

Status `asset_data`:
| Dari | Ke | Trigger |
|---|---|---|
| tersedia @gudang | proses | Ajukan peminjaman |
| proses | tersedia @gudang | Asset ditolak admin / peminjaman dibatalkan karyawan (pengajuan: semua item; dikonfirmasi: item disetujui) |
| proses @gudang | dipinjam @karyawan | Terima Asset |
| dipinjam @karyawan | tersedia @gudang | Pengembalian, kondisi normal |
| dipinjam @karyawan | rusak @gudang | Pengembalian, kondisi rusak |

---

## 16. Struktur Database

| Table | Type | Fungsi |
|---|---|---|
| `asset_trx_peminjaman_karyawan` | Transaction Header | Peminjaman & pengembalian (`jenis_status`) |
| `asset_trx_peminjaman_karyawan_item` | Transaction Detail | Asset per transaksi + keputusan/kondisi |
| `asset_data` | Master | Status & pemegang asset |
| `asset_kategori_m` | Master | Nama kategori (snapshot) |
| `data_karyawan`, `master_cabang`, `master_departemen`, `master_jabatan` | Master HR | Karyawan aktif, cabang, departemen, jabatan |
| `asset_admin` | Master akses | Admin asset |
| `master_company` | Master | Lock transaksi |
| `asset_trx_log` | Log | `peminjaman_karyawan`, `pengembalian_karyawan` |
| `asset_trx_offboarding_relasi` | Relasi | Menandai pengembalian yang dibuat offboarding |
| `notif_data`, `users` | Notifikasi | Inbox & token FCM |

> ⚠️ Belum ditemukan: migration `CREATE TABLE` untuk tabel peminjaman; struktur mengacu DBML + migrations `2026_09_17_000006`, `2026_09_17_000009`.

> ⚠️ **Prasyarat deploy v1.6:** nilai enum `dibatalkan` ditambahkan lewat SQL manual `Claude outputs/sql-peminjaman-status-dibatalkan.sql` (`ALTER TABLE … MODIFY COLUMN status ENUM('pengajuan','dikonfirmasi','ditolak','aktif','selesai','dibatalkan')`, nilai ditambahkan di akhir sehingga hanya metadata yang berubah). DBML `Enum status_peminjaman` sudah bertanda `[NEW KRY05]`. Dokumen ini tidak dapat memastikan SQL sudah dijalankan di server mana pun; jalankan **sebelum** API v1.6 dipakai (RISK-010).

---

## 17. Relasi Database

```mermaid
erDiagram
    ASSET_TRX_PEMINJAMAN_KARYAWAN ||--o{ ASSET_TRX_PEMINJAMAN_KARYAWAN_ITEM : "id_asset_trx_peminjaman"
    ASSET_TRX_PEMINJAMAN_KARYAWAN |o--o{ ASSET_TRX_PEMINJAMAN_KARYAWAN : "id_peminjaman_asal (pengembalian)"
    ASSET_DATA ||--o{ ASSET_TRX_PEMINJAMAN_KARYAWAN_ITEM : "id_asset_data"
    DATA_KARYAWAN ||--o{ ASSET_TRX_PEMINJAMAN_KARYAWAN : "id_karyawan (logis)"
    ASSET_DATA ||--o{ ASSET_TRX_LOG : "id_asset_data"
    ASSET_TRX_PEMINJAMAN_KARYAWAN ||--o{ ASSET_TRX_LOG : "id_transaksi (logis)"
    ASSET_TRX_PEMINJAMAN_KARYAWAN ||--o| ASSET_TRX_OFFBOARDING_RELASI : "id_tabel (logis)"

    ASSET_TRX_PEMINJAMAN_KARYAWAN {
        varchar id_asset_trx_peminjaman_karyawan PK
        varchar no_trx
        date tgl_peminjaman
        date tgl_dikembalikan
        varchar keperluan
        varchar id_karyawan
        enum jenis_status
        varchar id_peminjaman_asal FK
        enum status
        varchar id_company
    }
    ASSET_TRX_PEMINJAMAN_KARYAWAN_ITEM {
        varchar id_asset_trx_peminjaman_item PK
        varchar id_asset_trx_peminjaman FK
        varchar id_asset_data FK
        enum status_item
        varchar ket_tolak
    }
    ASSET_DATA {
        varchar id_asset_data PK
        enum status_asset
        enum pemegang_asset
        varchar id_pemegang_asset
    }
    ASSET_TRX_LOG {
        varchar id_asset_trx_log PK
        varchar id_asset_data FK
        enum jenis_log
        varchar id_transaksi
    }
```

Semua JOIN mencocokkan `id_company`. `id_peminjaman_asal` memiliki `ref` di DBML (self-reference).

---

## 18. Data Dictionary

### 18.1 asset_trx_peminjaman_karyawan
| Field | Type | Null | Default | Key | Description |
|---|---|:---:|---|---|---|
| id_asset_trx_peminjaman_karyawan | varchar(50) | No | – | PK | `id_company + YYYYMM + 3 digit` |
| no_trx | varchar(50) | No | '' | – | `KRY05-YYYYMMNNN` (tidak unique) |
| tgl_peminjaman | date | No | CURRENT_DATE | – | Tanggal pinjam (API: `tgl_pinjam`) |
| tgl_dikembalikan | date | No | CURRENT_DATE | – | Tanggal wajib kembali (API: `tgl_kembali`) |
| keperluan | varchar(255) | No | '' | – | `kantor`/`dinas`/`pelatihan`/`lainnya` |
| tujuan_peminjaman | varchar(255) | No | '' | – | Teks tujuan |
| tgl_serah_terima | date | No | 0000-00-00 | – | Jadwal serah terima (admin) |
| jam_serah_terima | time | No | 00:00:00 | – | Jam serah terima |
| keterangan_serah_terima | varchar(255) | – | '' | – | Catatan admin |
| ket_tolak | varchar(255) | No | '' | – | "Seluruh asset peminjaman ditolak." (status `ditolak`) / "Dibatalkan oleh karyawan." (status `dibatalkan`; data lama: `ditolak`) |
| id_karyawan, nik_karyawan, nama_karyawan | varchar | – | '' | IDX | Peminjam (snapshot) |
| jenis_status | enum(peminjaman, pengembalian) | No | peminjaman | IDX | Jenis baris |
| id_peminjaman_asal | varchar(50) | Yes | NULL | FK | Wajib untuk pengembalian |
| status | enum(pengajuan, dikonfirmasi, ditolak, aktif, selesai, **dibatalkan**) | No | pengajuan | IDX | API: `status_approval`. `dibatalkan` ditambah v1.6 (SQL manual) |
| user_approval / tgl_approval | varchar / timestamp | – | '' / NULL | – | Admin pemutus / pemeriksa pengembalian |
| id_company | varchar(50) | No | '' | IDX | Company |
| tgl_input, user_input | – | – | – | IDX | Pembuat (API: `tgl_pengajuan`) |
| tgl_update, user_update | – | Yes | NULL | – | Perubahan terakhir; dipakai sebagai **tanggal diterima** saat `aktif`, sebagai waktu pembatalan saat `dibatalkan`, dan sebagai urutan tab Riwayat saat `selesai` |

### 18.2 asset_trx_peminjaman_karyawan_item
| Field | Type | Null | Default | Key | Description |
|---|---|:---:|---|---|---|
| id_asset_trx_peminjaman_item | varchar(50) | No | – | PK | `id_company + YYYYMM + 3 digit` |
| id_asset_trx_peminjaman | varchar(50) | – | – | FK, IDX | → header |
| id_asset_data | varchar(50) | – | – | FK, IDX | → asset_data |
| nama_asset, deskripsi_asset, kategori_asset | varchar(255) | – | '' | – | Snapshot |
| status_item | enum(disetujui, ditolak) | Yes | NULL | IDX | Keputusan (peminjaman) / kondisi (pengembalian) |
| ket_tolak | varchar(255) | No | '' | – | Alasan tolak / keterangan kerusakan |
| id_asset_lokasi_m_tujuan | varchar(50) | No | '' | – | Lokasi gudang tujuan asset (pengembalian), dipilih admin (v1.1; migration `2026_10_05_000003`, SQL `sql-lokasi-retur-pengembalian.sql`) |
| id_company, tgl_input, user_input | – | – | – | IDX | – |

### 18.3 asset_data (field yang dipakai)
`status_asset` (tersedia → proses → dipinjam → tersedia/rusak), `pemegang_asset` (gudang ↔ karyawan), `id_pemegang_asset`, `id_asset_lokasi_m` (lokasi gudang; dikosongkan saat dipinjam, diisi saat kembali), `ket_rusak` (diisi saat kembali rusak), `kondisi_awal` (disalin ke log), `nama_asset`, `kode_asset`, `merek`, `deskripsi`, `tgl_update`, `user_update`.

### 18.4 asset_trx_log
| Event | jenis_log | kondisi_awal → kondisi_akhir | posisi_awal → akhir | pemegang |
|---|---|---|---|---|
| Terima Asset | `peminjaman_karyawan` | kondisi_awal → kondisi_awal | gudang → karyawan | karyawan (id, nama) |
| Pengembalian | `pengembalian_karyawan` | kondisi_awal → normal/rusak | karyawan → gudang | gudang (kosong) |

---

## 19. Database Index

| Index | Table | Columns | Tujuan |
|---|---|---|---|
| `atpk_admin_list_idx` | header | (id_company, jenis_status, status, tgl_input, id_asset_trx_peminjaman_karyawan) | List admin per jenis & tab |
| `atpk_employee_list_idx` | header | (id_company, id_karyawan, jenis_status, status, tgl_input, id_asset_trx_peminjaman_karyawan) | List karyawan |
| `atpki_loan_status_idx` | item | (id_asset_trx_peminjaman, id_company, status_item, id_asset_data) | Hitung item, cek asset per header |

Dibuat migration `2026_09_17_000006`, dibuat ulang `2026_09_17_000009`. Index tidak berubah di v1.6 (SQL `dibatalkan` tidak menyentuh index). Filter `kondisi` (tgl kembali) dan urutan tab Riwayat (`tgl_update`) tidak memiliki index khusus (⚠️ belum diukur). Pencarian `hasActivePeminjamanAsset`/`hasActivePengembalianAsset` berdasarkan `atpki.id_asset_data` tidak diawali kolom index (⚠️ belum diukur).

---

## 20. Backend Architecture

```text
RouteAsset.php (prefix /asset, jwt.api + jwt.ctx)
   ↓
AssetAdminPeminjaman_c + AssetAdminPengembalian_c (admin) / AssetKaryawanPeminjaman_c (karyawan; terima & batalkan lewat prosesSerahTerimaPeminjaman)
   ↓ validateRequest + accessError
AssetPeminjamanKaryawan_m ← Asset_m (lock, sequence, getLockedAssets, findActiveKaryawan, findAssetAdmin, hasActiveOnboardingAsset)
   ↓                      ← AssetOffboardingKaryawan_m::hasActiveOffboardingAsset
DB::transaction (retry 3)
   ↓
ApiResponse + AssetNotification (setelah commit)
```

| Layer | File/Class | Function |
|---|---|---|
| Route | `RouteAsset.php` | 15 route peminjaman/pengembalian |
| Controller | `AssetAdminPeminjaman_c` (peminjaman), `AssetAdminPengembalian_c` (pengembalian) | getPeminjamanKaryawan, getDetailPeminjamanKaryawan, konfirmasiPeminjamanKaryawan, getPengembalianKaryawan, getDetailPengembalianKaryawan, konfirmasiPengembalianKaryawan |
| Controller | `AssetKaryawanPeminjaman_c` | getPeminjamanOptions, getPeminjamanSaya, storePeminjamanSaya, getDetailPeminjamanSaya, terimaPeminjamanSaya, batalkanPeminjamanSaya, ajukanPengembalianSaya, getPengembalianSaya, getDetailPengembalianSaya |
| Model | `AssetPeminjamanKaryawan_m` | getOptions, getPeminjaman/Page, getPengembalian(Admin), getDetail(Admin), getDetailPengembalian(Admin), store, konfirmasiAdmin, terimaKaryawan, batalkanKaryawan, ajukanPengembalianKaryawan, konfirmasiPengembalianAdmin (lokasi gudang per item via `resolveItemLocations` di `Asset_m`), getPendingForEmployee, getPendingForAdmin, countPendingForAdmin, getJatuhTempo, countJatuhTempo; v1.6: `applyKondisiAktifFilter`, `attachRiwayatPengembalian`, `pengembalianForDetail`, `peminjamanFilters` |
| Enum | `PeminjamanStatus` | pengajuan/dikonfirmasi/ditolak/aktif/selesai/dibatalkan (dipakai lewat konstanta model) |
| Support | `AssetNotification` | Event `peminjaman.*`, `pengembalian.*` |

---

## 21. Backend Business Logic

### 21.1 Ajukan — `store`
Lock → karyawan aktif → `getLockedAssets` (tersedia + gudang) → tidak aktif di onboarding/peminjaman → tgl_kembali ≥ tgl_pinjam → ID + `KRY05-…` → INSERT header (`pengajuan`, jadwal kosong) → INSERT item (`status_item` NULL) → UPDATE asset `proses` → notif `peminjaman.diajukan`. Response 201.

### 21.2 Konfirmasi admin — `konfirmasiAdmin`
Lock → admin → header peminjaman FOR UPDATE → bukan milik offboarding → status `pengajuan` → semua item diputuskan (tolak wajib alasan) → UPDATE item → bila ada disetujui: jadwal wajib, format valid, di dalam periode pinjam → UPDATE header (`dikonfirmasi`/`ditolak`, jadwal atau nol, `ket_tolak`, `user_approval`, `tgl_approval`) → asset ditolak → `tersedia` @gudang → notif `peminjaman.disetujui`/`ditolak`. Response 200 (+`jumlah_disetujui`, `jumlah_ditolak`).

### 21.3 Terima — `terimaKaryawan`
Lock → header milik karyawan FOR UPDATE → status `dikonfirmasi` → karyawan aktif → item disetujui → asset harus `proses` @gudang → UPDATE asset `dipinjam` @karyawan (`id_asset_lokasi_m = NULL`) → header `aktif` → INSERT `asset_trx_log peminjaman_karyawan` → notif `peminjaman.diterima`.

### 21.4 Batalkan — `batalkanKaryawan` (v1.6)
Lock company → header milik karyawan FOR UPDATE (`lockedEmployeeHeader`, hanya `jenis_status = peminjaman`) → status harus `pengajuan` **atau** `dikonfirmasi` → karyawan aktif → ambil asset item: dari `pengajuan` semua item dengan `status_item` NULL, dari `dikonfirmasi` item `disetujui` (FOR UPDATE) → UPDATE `asset_data` menjadi `tersedia` @gudang (`id_pemegang_asset = ''`) **hanya** yang masih `proses` + `gudang` → header `dibatalkan`, `ket_tolak = "Dibatalkan oleh karyawan."`, `tgl_update`, `user_update` → notif `peminjaman.dibatalkan`. Tidak ada `asset_trx_log`. Controller: `batalkanPeminjamanSaya` → `prosesSerahTerimaPeminjaman($diterima = false)`. Response 200.

### 21.5 Ajukan pengembalian — `ajukanPengembalianKaryawan`
Lock → peminjaman milik karyawan `aktif` → item disetujui → semua asset masih `dipinjam` oleh karyawan → tidak ada pengembalian `pengajuan`/`dikonfirmasi` → tidak di offboarding → INSERT header `jenis_status = pengembalian`, `id_peminjaman_asal`, salinan periode/keperluan, `pengajuan` → INSERT item (salinan item disetujui, `status_item` NULL) → notif `pengembalian.diajukan`. Response 201.

### 21.6 Konfirmasi pengembalian — `konfirmasiPengembalianAdmin`
Lock → admin → header pengembalian `pengajuan` → peminjaman asal ada & `aktif` → semua item diisi kondisi + lokasi gudang (`resolveItemLocations`) → asset masih `dipinjam` oleh karyawan → per item: UPDATE item (`disetujui` = normal / `ditolak` = rusak + keterangan, `id_asset_lokasi_m_tujuan`), UPDATE asset (`tersedia`/`rusak` @gudang, `id_asset_lokasi_m` pilihan admin, `ket_rusak`), siapkan `asset_trx_log pengembalian_karyawan` → INSERT log → header pengembalian `selesai` (+`user_approval`, `tgl_approval`) → bila tidak ada asset pinjaman yang masih dipegang: peminjaman asal `selesai` → notif `pengembalian.dikonfirmasi`. Response 200 (+`jumlah_normal`, `jumlah_rusak`).

### 21.7 Query baca
- **Options**: asset `tersedia` @gudang yang tidak ada di peminjaman berjalan (item tidak ditolak) + daftar keperluan.
- **List peminjaman**: tab Permintaan/Sedang Dipinjam tidak menampilkan `selesai`, dan `aktif` hanya jika asset masih dipegang karyawan. Filter tab: `pengajuan`, `dikonfirmasi`, `ditolak`, `aktif`, `dibatalkan`, `riwayat` (v1.6). Tab tanpa nilai (hanya `limit`/`offset`) tidak memfilter status, kecuali `selesai` tetap disembunyikan.
- **Kondisi (v1.6)**: `applyKondisiAktifFilter` hanya dipanggil bila `tab=aktif`; `terlambat` = `tgl_dikembalikan` < hari ini, `jatuh_tempo` = hari ini sampai besok; diterapkan di query SQL sebelum `limit+1`/`offset`.
- **Riwayat (v1.6)**: `peminjamanQuery(includeSelesai)` + filter `status = selesai`, `reorder()` ke `tgl_update` desc, id desc; `attachRiwayatPengembalian` mengambil pengembalian `selesai` untuk seluruh id di halaman dengan satu query (JOIN item, `SUM` normal/rusak) dan hanya menempelkan yang `id_karyawan`-nya sama dengan peminjaman. Pengembalian offboarding tidak dikecualikan.
- **Detail peminjaman (v1.6)**: key `pengembalian` dari `pengembalianForDetail` (semua status, urut `tgl_input` asc, `id_karyawan` sama) yang memanggil `getDetail(..., includeOffboarding = true)` per pengembalian.
- **List pengembalian**: mengecualikan pengembalian milik offboarding (tab/list/detail Pengembalian).
- **Detail**: menghitung `terlambat`, `tgl_diterima` (= `tgl_update` saat aktif), `dapat_dikembalikan` (hanya untuk karyawan).
- **Jatuh tempo**: aktif, `tgl_dikembalikan ≤ besok`, asset masih dipegang, belum ada pengembalian berjalan; `sisa_hari` negatif = terlambat.

---

## 22. Dampak terhadap asset_data

| Field | Sebelum | Sesudah | Trigger |
|---|---|---|---|
| status_asset | tersedia | proses | Ajukan peminjaman |
| status_asset / pemegang | proses | tersedia / gudang | Asset ditolak admin; peminjaman dibatalkan (pengajuan: semua item; dikonfirmasi: item disetujui; hanya asset `proses` + gudang) |
| status_asset | proses | dipinjam | Terima Asset |
| pemegang_asset / id_pemegang_asset | gudang / '' | karyawan / id_karyawan | Terima Asset |
| id_asset_lokasi_m | lokasi gudang | NULL (v1.1) | Terima Asset |
| status_asset | dipinjam | tersedia | Pengembalian normal |
| status_asset | dipinjam | rusak | Pengembalian rusak |
| pemegang_asset / id_pemegang_asset | karyawan / id | gudang / '' | Pengembalian |
| id_asset_lokasi_m | NULL | lokasi gudang pilihan admin (v1.1) | Pengembalian |
| ket_rusak | – | keterangan (rusak) / '' (normal) | Pengembalian |
| tgl_update / user_update | – | now / pelaku | Setiap perubahan |

---

## 23. API Documentation

| ID | Method | Endpoint | Function | Auth |
|---|---|---|---|---|
| API-001 | GET | `/asset/peminjaman-karyawan` | List peminjaman (admin); `tab` termasuk `dibatalkan`/`riwayat`, `kondisi` | Admin |
| API-002 | GET | `/asset/peminjaman-karyawan/{id}` | Detail peminjaman (admin) + `pengembalian[]` | Admin |
| API-003 | POST | `/asset/peminjaman-karyawan/{id}/konfirmasi` | Setujui/tolak per asset + jadwal | Admin |
| API-004 | GET | `/asset/pengembalian-karyawan` | List pengembalian (admin) | Admin |
| API-005 | GET | `/asset/pengembalian-karyawan/{id}` | Detail pengembalian (admin) | Admin |
| API-006 | POST | `/asset/pengembalian-karyawan/{id}/konfirmasi` | Konfirmasi kondisi + lokasi gudang per asset | Admin |
| API-007 | GET | `/asset/karyawan/peminjaman/options` | Opsi keperluan & asset | JWT |
| API-008 | GET | `/asset/karyawan/peminjaman` | List peminjaman saya; `tab` termasuk `dibatalkan`/`riwayat`, `kondisi` | JWT |
| API-009 | POST | `/asset/karyawan/peminjaman` | Ajukan peminjaman | JWT |
| API-010 | GET | `/asset/karyawan/peminjaman/{id}` | Detail peminjaman saya + `pengembalian[]` | JWT, pemilik |
| API-011 | POST | `/asset/karyawan/peminjaman/{id}/terima` | Terima asset | JWT, pemilik |
| API-012 | POST | `/asset/karyawan/peminjaman/{id}/batalkan` | Batalkan (status pengajuan atau dikonfirmasi) | JWT, pemilik |
| API-013 | POST | `/asset/karyawan/peminjaman/{id}/pengembalian` | Ajukan pengembalian | JWT, pemilik |
| API-014 | GET | `/asset/karyawan/pengembalian` | List pengembalian saya | JWT |
| API-015 | GET | `/asset/karyawan/pengembalian/{id}` | Detail pengembalian saya | JWT, pemilik |

Terkait: `GET /asset/karyawan/onboarding` (Asset Saya: `peminjaman[]`, `jatuh_tempo[]`, `peminjaman_aktif[]`) dan `GET /asset/summary` (`peminjaman_perlu_proses`, `peminjaman_pending`, `jatuh_tempo_jumlah`, `jatuh_tempo`).
Tolak pengembalian dan perpanjangan: ⚠️ Belum ditemukan. (Batalkan pengajuan tersedia sejak v1.6.)

---

## 24. Detail Setiap API

Header umum: `Authorization: Bearer <token>`, `Content-Type: application/json`, `Accept`, `locale`, `versi-app`. Format response `{ code, message, data }`.

### API-001 / API-008 — List Peminjaman
```http
GET /asset/peminjaman-karyawan?tab=pengajuan&limit=20&offset=0          (admin)
GET /asset/karyawan/peminjaman?tab=aktif&kondisi=terlambat&limit=20&offset=0   (karyawan)
GET /asset/karyawan/peminjaman?tab=riwayat&limit=20&offset=0            (karyawan)
```
| Parameter | Nilai | Keterangan |
|---|---|---|
| `tab` | `pengajuan` \| `dikonfirmasi` \| `ditolak` \| `aktif` \| `dibatalkan` \| `riwayat` | Selain itu 422 "Filter peminjaman asset tidak valid." `dibatalkan` dan `riwayat` baru di v1.6 |
| `kondisi` | `semua` \| `jatuh_tempo` \| `terlambat` | Opsional, baru v1.6. Hanya efektif bila `tab=aktif` (diabaikan di tab lain). `terlambat`: `tgl_kembali` < hari ini; `jatuh_tempo`: `tgl_kembali` hari ini sampai besok (terlambat tidak masuk); `semua`/kosong: semua aktif. Difilter di SQL sebelum paginasi. Nilai lain 422 |
| `limit` | 1–50 (default 20) | "Limit maksimal adalah 50 data." |
| `offset` | ≥ 0 (default 0) | – |

Tanpa `tab`, `limit`, dan `offset` respons berupa array (non-paginasi) dan `kondisi` diabaikan. Kegagalan validasi: HTTP 422, message "Parameter daftar peminjaman asset tidak valid.", `errors.kondisi = ["Filter kondisi peminjaman asset tidak valid."]` (EN: "Invalid asset loan condition filter."), `errors.tab = ["Filter peminjaman asset tidak valid."]`.

```json
{
  "code": "SUCCESS",
  "message": "Data peminjaman asset berhasil ditemukan.",
  "data": {
    "filters": [
      {"value": "pengajuan", "label": "Pengajuan"},
      {"value": "dikonfirmasi", "label": "Dikonfirmasi"},
      {"value": "ditolak", "label": "Ditolak"},
      {"value": "aktif", "label": "Aktif"},
      {"value": "dibatalkan", "label": "Dibatalkan"}
    ],
    "records": [
      {
        "id": "CMP001202609007",
        "no_trx": "KRY05-202609007",
        "tgl_pengajuan": "2026-09-26",
        "keperluan": "dinas",
        "tujuan_peminjaman": "Presentasi klien di Surabaya",
        "tgl_pinjam": "2026-09-28",
        "tgl_kembali": "2026-10-02",
        "tgl_serah_terima": "2026-09-28",
        "jam_serah_terima": "08:30:00",
        "keterangan_serah_terima": "Ambil di ruang IT",
        "ket_tolak": "",
        "id_karyawan": "KRY0003",
        "nik_karyawan": "EMP-003",
        "nama_karyawan": "Andi Contoh",
        "jenis_status": "peminjaman",
        "id_peminjaman_asal": "",
        "status_approval": "aktif",
        "status_label": "Aktif",
        "tab": "aktif",
        "jumlah_asset": 1,
        "tgl_diterima": "2026-09-28",
        "terlambat": false
      }
    ],
    "pagination": {"limit": 20, "offset": 0, "next_offset": 1, "has_more": false}
  }
}
```
`filters` sengaja **tidak** memuat `riwayat` (tab Riwayat punya tab utama sendiri di mobile).

**Tab `riwayat` (v1.6).** Hanya peminjaman `selesai` (company login; karyawan login untuk endpoint karyawan), urut `tgl_update` desc lalu `id` desc, paginasi `limit+1` tanpa COUNT. Setiap record memuat field di atas plus `pengembalian[]` (urut `tgl_approval` asc; hanya pengembalian `selesai` milik peminjaman dan karyawan yang sama; satu query per halaman, tanpa N+1). Pengembalian yang berasal dari offboarding **ikut** ditampilkan (keputusan produk).
```json
{
  "id": "CMP001202610001",
  "no_trx": "KRY05-202610001",
  "status_approval": "selesai",
  "status_label": "Selesai",
  "tab": "selesai",
  "jumlah_asset": 2,
  "tgl_diterima": "",
  "terlambat": false,
  "pengembalian": [
    {"id": "CMP001202610002", "no_trx": "KRY05-202610002", "tgl_selesai": "2026-10-28", "jumlah_normal": 0, "jumlah_rusak": 1}
  ]
}
```
(field header lain sama dengan contoh di atas). `tgl_selesai` = tanggal `tgl_approval` pengembalian (`""` bila kosong); `jumlah_normal` = item `disetujui`, `jumlah_rusak` = item `ditolak`.

### API-002 / API-010 — Detail Peminjaman
```http
GET /asset/peminjaman-karyawan/{id}        (admin)
GET /asset/karyawan/peminjaman/{id}        (karyawan, hanya miliknya)
```
Field sama dengan record list (tanpa `id_karyawan`, `nik/nama` di dalam `karyawan`) ditambah:
```json
{
  "dapat_dikembalikan": true,
  "karyawan": {"nik": "EMP-003", "nama_karyawan": "Andi Contoh", "cabang": "Kantor Pusat", "divisi": "Sales", "jabatan": "Staff Sales"},
  "assets": [
    {
      "id_item": "CMP001202609021",
      "id_asset_data": "CMP00120260100009",
      "kode_asset": "PRJ-0002",
      "nama_asset": "Proyektor Mini",
      "deskripsi": "Kabel HDMI, tas",
      "kategori_asset": "Proyektor",
      "merek": "MerekY",
      "status_item": "disetujui",
      "ket_tolak": "",
      "id_asset_lokasi_m_tujuan": "",
      "nama_lokasi_tujuan": ""
    }
  ],
  "pengembalian": [
    {
      "id": "CMP001202610002",
      "no_trx": "KRY05-202610002",
      "status_approval": "selesai",
      "status_label": "Selesai",
      "tgl_pengajuan": "2026-10-28",
      "tgl_selesai": "2026-10-28",
      "assets": [
        {
          "id_item": "CMP001202610003",
          "id_asset_data": "CMP00120260100009",
          "nama_asset": "Proyektor Mini",
          "kondisi": "rusak",
          "keterangan_kerusakan": "Lensa tergores",
          "lokasi_tujuan": "Gudang Servis"
        }
      ]
    }
  ]
}
```
`dapat_dikembalikan` hanya bernilai true pada endpoint karyawan. 404: "Peminjaman asset tidak ditemukan."

**`pengembalian[]` (v1.6)** hanya ada pada detail peminjaman (bukan detail pengembalian), boleh kosong, berisi **semua** status pengembalian milik peminjaman itu (urut `tgl_input` asc, `id_karyawan` sama dengan peminjaman). `tgl_selesai` berisi tanggal `tgl_approval` hanya bila status `selesai`, selain itu `""`. `assets[].kondisi`: `normal` (dari `disetujui`), `rusak` (dari `ditolak`), selain itu `""`; `keterangan_kerusakan` = `ket_tolak` item; `lokasi_tujuan` = nama lokasi gudang atau `""`. Pengembalian dari offboarding ikut tampil. Backend memanggil `getDetail` per pengembalian (RISK-012).

### API-003 — Konfirmasi Peminjaman (Admin)
```http
POST /asset/peminjaman-karyawan/{id}/konfirmasi
```
```json
{
  "items": [
    {"id_item": "CMP001202609021", "status_item": "disetujui"},
    {"id_item": "CMP001202609022", "status_item": "ditolak", "ket_tolak": "Sedang dijadwalkan servis"}
  ],
  "tgl_serah_terima": "2026-09-28",
  "jam_serah_terima": "08:30",
  "keterangan_serah_terima": "Ambil di ruang IT"
}
```
| Field | Rule | Message |
|---|---|---|
| items | required\|array\|min:1 | Keputusan asset wajib diisi. |
| items.*.id_item | required\|string\|max:50\|distinct | Asset yang sama tidak boleh dikirim lebih dari sekali. |
| items.*.status_item | required\|in:disetujui,ditolak | Keputusan setiap asset wajib dipilih. / Keputusan asset harus disetujui atau ditolak. |
| items.*.ket_tolak | nullable\|max:255\|required_if ditolak | Alasan penolakan asset wajib diisi. |
| tgl_serah_terima | nullable\|date_format:Y-m-d (wajib di model bila ada disetujui) | Format tanggal serah terima harus YYYY-MM-DD. |
| jam_serah_terima | nullable\|date_format:H:i (wajib di model bila ada disetujui) | Format jam serah terima harus HH:MM. |
| keterangan_serah_terima | nullable\|max:255 | – |

Database impact:
```text
UPDATE asset_trx_peminjaman_karyawan_item (status_item, ket_tolak)
UPDATE asset_trx_peminjaman_karyawan (status, jadwal, ket_tolak, user_approval, tgl_approval)
UPDATE asset_data (item ditolak → tersedia @gudang)
INSERT notif_data
```
```json
{ "code": "UPDATED", "message": "Peminjaman asset berhasil dikonfirmasi.",
  "data": { "id": "CMP001202609007", "no_trx": "KRY05-202609007", "status_approval": "dikonfirmasi",
            "status_label": "Dikonfirmasi", "tab": "dikonfirmasi", "jumlah_disetujui": 1, "jumlah_ditolak": 1 } }
```
Semua ditolak: "Peminjaman asset berhasil ditolak.", `status_approval: ditolak`.
422: "Peminjaman asset tidak ditemukan.", "Pengembalian ini diproses melalui offboarding.", "Peminjaman asset sudah tidak menunggu konfirmasi.", "Seluruh asset peminjaman wajib dikonfirmasi.", "Tanggal/Jam serah terima wajib diisi.", "Format jadwal serah terima tidak valid.", "Tanggal serah terima harus berada dalam periode peminjaman." 500: "Terjadi kesalahan saat mengonfirmasi peminjaman asset."

### API-004 / API-014 — List Pengembalian
```http
GET /asset/pengembalian-karyawan?tab=pengajuan     (admin)
GET /asset/karyawan/pengembalian?tab=selesai       (karyawan)
```
`tab`: `pengajuan` (pengajuan/dikonfirmasi) | `selesai` (ditolak/selesai). Filters: Pengajuan, Selesai. Record sama dengan API-001 dengan `jenis_status = pengembalian` dan `id_peminjaman_asal` terisi.

### API-005 / API-015 — Detail Pengembalian
```http
GET /asset/pengembalian-karyawan/{id}
GET /asset/karyawan/pengembalian/{id}
```
Struktur seperti API-002; `assets[].status_item` bernilai `normal`/`rusak` (dipetakan dari `disetujui`/`ditolak`), `ket_tolak` = keterangan kerusakan. 404: "Pengembalian asset tidak ditemukan."

### API-006 — Konfirmasi Pengembalian (Admin)
```http
POST /asset/pengembalian-karyawan/{id}/konfirmasi
```
```json
{
  "items": [
    {"id_item": "CMP001202610003", "kondisi": "rusak", "id_asset_lokasi_m": "CMP001202610001", "keterangan_kerusakan": "Lensa tergores"}
  ]
}
```
| Field | Rule | Message |
|---|---|---|
| items | required\|array\|min:1 | Kondisi asset wajib diisi. |
| items.*.id_item | required\|distinct | Asset yang sama tidak boleh dikirim lebih dari sekali. |
| items.*.kondisi | required\|in:normal,rusak | Kondisi setiap asset wajib dipilih. / Kondisi asset harus normal atau rusak. |
| items.*.keterangan_kerusakan | nullable\|max:255\|required_if rusak | Keterangan kerusakan asset wajib diisi. |
| items.*.id_asset_lokasi_m | required\|string\|max:50 | Lokasi gudang tujuan wajib dipilih. (v1.1) |

Database impact:
```text
UPDATE asset_trx_peminjaman_karyawan_item (status_item, ket_tolak, id_asset_lokasi_m_tujuan)
UPDATE asset_data (tersedia / rusak @gudang, id_asset_lokasi_m, ket_rusak)
INSERT asset_trx_log (pengembalian_karyawan)
UPDATE asset_trx_peminjaman_karyawan (pengembalian → selesai; peminjaman asal → selesai)
INSERT notif_data
```
```json
{ "code": "UPDATED", "message": "Pengembalian asset berhasil dikonfirmasi.",
  "data": { "id": "CMP001202610001", "no_trx": "KRY05-202610001", "jenis_status": "pengembalian",
            "id_peminjaman_asal": "CMP001202609007", "status_approval": "selesai", "status_label": "Selesai",
            "tab": "selesai", "jumlah_normal": 0, "jumlah_rusak": 1 } }
```
422: "Pengembalian asset tidak ditemukan.", "Pengembalian asset sudah tidak menunggu konfirmasi.", "Peminjaman asal pengembalian tidak ditemukan.", "Peminjaman asal pengembalian tidak ditemukan atau sudah tidak aktif.", "Seluruh kondisi asset pengembalian wajib diisi.", "Satu atau beberapa asset tidak sedang dipinjam oleh karyawan.", "Lokasi gudang tujuan setiap asset wajib dipilih.", "Lokasi gudang tujuan tidak valid.", "Lokasi gudang tujuan asset tidak sesuai." 500: "Terjadi kesalahan saat mengonfirmasi pengembalian asset."

Detail pengembalian admin (API-005) mulai v1.1 memuat `lokasi_gudang[]` (gudang milik company), `karyawan.id_cabang`, dan `assets[].id_asset_lokasi_m_tujuan` serta `nama_lokasi_tujuan` (kosong sampai dikonfirmasi). Pesan umum validasi: "Data konfirmasi pengembalian belum lengkap."

### API-007 — Opsi Peminjaman
```http
GET /asset/karyawan/peminjaman/options
```
```json
{
  "code": "SUCCESS",
  "message": "Opsi peminjaman asset berhasil ditemukan.",
  "data": {
    "keperluan": [
      {"value": "kantor", "label": "Kantor"},
      {"value": "dinas", "label": "Dinas"},
      {"value": "pelatihan", "label": "Pelatihan"},
      {"value": "lainnya", "label": "Lainnya"}
    ],
    "assets": [
      {"id_asset_data": "CMP00120260100009", "kode_asset": "PRJ-0002", "nama_asset": "Proyektor Mini",
       "kategori_asset": "Proyektor", "merek": "MerekY", "deskripsi": "Kabel HDMI, tas"}
    ]
  }
}
```

### API-009 — Ajukan Peminjaman
```http
POST /asset/karyawan/peminjaman
```
```json
{
  "keperluan": "dinas",
  "tujuan_peminjaman": "Presentasi klien di Surabaya",
  "tgl_pinjam": "2026-09-28",
  "tgl_kembali": "2026-10-02",
  "asset_ids": ["CMP00120260100009"]
}
```
| Field | Rule | Message |
|---|---|---|
| keperluan | required\|string\|max:255 | Keperluan wajib diisi. |
| tujuan_peminjaman | required\|string\|max:255 | Tujuan peminjaman wajib diisi. |
| tgl_pinjam | required\|date_format:Y-m-d\|after_or_equal:today | Tanggal pinjam wajib diisi. / Format … / Tanggal pinjam tidak boleh sebelum hari ini. |
| tgl_kembali | required\|date_format:Y-m-d\|after_or_equal:tgl_pinjam | Tanggal kembali wajib diisi. / Format … / Tanggal kembali tidak boleh sebelum tanggal pinjam. |
| asset_ids | required\|array\|min:1 | Asset yang dipinjam wajib dipilih. |
| asset_ids.* | required\|max:50\|distinct | Asset yang sama tidak boleh dipilih lebih dari sekali. |

> `keperluan` tidak divalidasi terhadap daftar opsi di backend (hanya string ≤255).

Database impact: `INSERT header (pengajuan)`, `INSERT item`, `UPDATE asset_data (proses)`, `INSERT notif_data`.
```json
{ "code": "CREATED", "message": "Pengajuan peminjaman asset berhasil dikirim.",
  "data": { "id": "CMP001202609007", "no_trx": "KRY05-202609007", "status_approval": "pengajuan",
            "status_label": "Menunggu Konfirmasi", "tab": "pengajuan" } }
```
422: "Karyawan tidak ditemukan atau tidak aktif.", "Satu atau beberapa asset tidak tersedia untuk dipinjam.", "…sedang diproses pada onboarding.", "…sedang diproses pada peminjaman lain.", "Nomor transaksi untuk periode ini sudah mencapai batas." 500: "Terjadi kesalahan saat menyimpan peminjaman asset."

### API-011 — Terima Asset
```http
POST /asset/karyawan/peminjaman/{id}/terima
```
Tanpa body. Database impact: `UPDATE asset_data (dipinjam @karyawan)`, `UPDATE header (aktif)`, `INSERT asset_trx_log (peminjaman_karyawan)`, `INSERT notif_data`.
```json
{ "code": "UPDATED", "message": "Asset peminjaman berhasil diterima.",
  "data": { "id": "CMP001202609007", "no_trx": "KRY05-202609007", "status_approval": "aktif",
            "status_label": "Aktif", "tab": "aktif", "jumlah_asset": 1 } }
```
422: "Peminjaman asset tidak ditemukan.", "Peminjaman asset belum dapat diterima.", "Karyawan tidak ditemukan atau tidak aktif.", "Tidak ada asset yang disetujui untuk diterima.", "Satu atau beberapa asset belum siap diserahterimakan." 500: "Terjadi kesalahan saat memproses serah terima asset."

### API-012 — Batalkan Peminjaman (v1.6: pengajuan dan dikonfirmasi)
```http
POST /asset/karyawan/peminjaman/{id}/batalkan
```
Tanpa body. Hanya pemilik transaksi (dari token) dan hanya baris `jenis_status = peminjaman`. Dari `pengajuan` seluruh asset item dilepas; dari `dikonfirmasi` hanya item `disetujui`. Hanya asset yang masih `proses` + `gudang` diubah menjadi `tersedia` @gudang. Database impact: `UPDATE asset_data (tersedia @gudang)`, `UPDATE header (dibatalkan, ket_tolak = "Dibatalkan oleh karyawan.", tgl_update, user_update)`, `INSERT notif_data`. **Tidak ada** `asset_trx_log`.
```json
{ "code": "UPDATED", "message": "Peminjaman asset berhasil dibatalkan.",
  "data": { "id": "CMP001202609007", "no_trx": "KRY05-202609007", "status_approval": "dibatalkan",
            "status_label": "Dibatalkan", "tab": "dibatalkan" } }
```
EN: "Asset loan cancelled successfully." Notifikasi `peminjaman.dibatalkan` ke seluruh admin.

422 (`BUSINESS_RULE_VIOLATION`): "Peminjaman asset sudah tidak dapat dibatalkan." (status `aktif`, `selesai`, `ditolak`, atau `dibatalkan`), "Peminjaman asset tidak ditemukan." (milik karyawan lain, id tidak ada, atau id adalah baris pengembalian), "Karyawan tidak ditemukan atau tidak aktif." 500: "Terjadi kesalahan saat memproses serah terima asset."

### API-013 — Ajukan Pengembalian
```http
POST /asset/karyawan/peminjaman/{id}/pengembalian
```
Tanpa body (semua asset disetujui ikut dikembalikan). Database impact: `INSERT header pengembalian`, `INSERT item`, `INSERT notif_data`.
```json
{ "code": "CREATED", "message": "Pengajuan pengembalian asset berhasil dikirim.",
  "data": { "id": "CMP001202610001", "no_trx": "KRY05-202610001", "jenis_status": "pengembalian",
            "id_peminjaman_asal": "CMP001202609007", "status_approval": "pengajuan",
            "status_label": "Menunggu Konfirmasi", "tab": "pengajuan", "jumlah_asset": 1 } }
```
422: "Peminjaman asset tidak ditemukan.", "Hanya peminjaman aktif yang dapat dikembalikan.", "Tidak ada asset aktif yang dapat dikembalikan.", "Satu atau beberapa asset tidak sedang dipinjam oleh karyawan.", "Pengembalian asset sedang menunggu konfirmasi.", "Asset sedang diproses pada offboarding."

---

## 25. Mobile Architecture

```text
Page → Bloc (AssetPeminjamanBloc / AssetPeminjamanAdminBloc) → Api (AssetPeminjamanApi / AssetPeminjamanAdminApi) → ApiClient → Backend
```

| Layer | File | Function |
|---|---|---|
| Api | `Repository/Service/assetPeminjamanApi.dart` | getPeminjaman, getOptions, store, getDetail, terima, batalkan, getPengembalian, getDetailPengembalian, ajukanPengembalian |
| Api | `Repository/Service/assetPeminjamanAdminApi.dart` | getPeminjaman, getDetail, konfirmasi, getPengembalian, getDetailPengembalian, konfirmasiPengembalian (+ `id_asset_lokasi_m` per item) |
| Model | `Repository/Model/Asset/mAssetPeminjaman.dart` | AssetPeminjamanAsset, AssetPeminjamanKeperluan, AssetPeminjamanOptions, AssetPeminjamanKaryawan, `AssetPeminjamanApproval` (kini termasuk `dibatalkan`) + `parseAssetPeminjamanApproval`, AssetPeminjamanRecord (+ `pengembalian`), `AssetPeminjamanPengembalian` & `AssetPeminjamanPengembalianAsset` (v1.6), ekstensi `AssetPeminjamanRecordPengembalianX` (`isSelesai`, jumlah normal/rusak, tanggal pengembalian terakhir), AssetPeminjamanPage, AssetPeminjamanRequest |
| Model | `Repository/Model/Asset/mAssetPeminjamanAdmin.dart` | AssetPeminjamanAdminKeputusan, AssetPeminjamanAdminKonfirmasiRequest, AssetPengembalianAdminKondisi (kini + `idLokasi`), AssetPengembalianAdminKonfirmasiRequest; model bersama `AssetWarehouseLocation` (`mAssetWarehouseLocation.dart`) |
| Bloc | `Bloc/Asset/AssetPeminjamanBloc.dart` | List peminjaman/pengembalian (`LoadAssetPeminjamanList(tab, kondisi)`; state menyimpan `activePeminjamanTab` & `activePeminjamanKondisi`; kondisi di-reset ke `semua` bila tab diberikan tanpa kondisi), options, detail, submit, terima, batalkan, ajukan pengembalian |
| Bloc | `Bloc/Asset/AssetPeminjamanAdminBloc.dart` | List, detail, konfirmasi peminjaman & pengembalian |
| UI karyawan | `UI/Modul/Asset/AssetSaya/Peminjaman/*` | HalamanAssetPeminjaman, HalamanTambahAssetPeminjaman, HalamanDetailAssetPeminjaman, AssetPeminjamanPicker, AssetPeminjamanWidgets (berisi `AssetPeminjamanPageMode`, `AssetPeminjamanKondisi`, `AssetPeminjamanTabBar`, `AssetPeminjamanRiwayatCard`, `AssetPeminjamanPengembalianSection`) |
| UI admin | `UI/Modul/Asset/AssetAdmin/AssetAdminKaryawan/Peminjaman/*` | HalamanAssetAdminPeminjaman, HalamanAssetAdminDetailPeminjaman, AssetPeminjamanAdminConfirmationSheet, AssetPeminjamanAdminWidgets |
| UI | `UI/Modul/Asset/AssetSaya/HalamanAssetSaya.dart` | Banner terima, pengingat jatuh tempo, kartu peminjaman aktif |

| Route | Page | Event awal |
|---|---|---|
| `/asset_peminjaman` | Peminjaman Asset (argumen `AssetPeminjamanPageMode`; `true` lama → Pengembalian, selain itu Permintaan Peminjaman; lihat §26.1) | Per mode: `LoadAssetPeminjamanList()` (peminjaman), `(tab: 'aktif')` (sedangDipinjam), `(tab: 'riwayat')` (riwayat), `LoadAssetPengembalianList()` (pengembalian) |
| `/asset_tambah_peminjaman` | Tambah Peminjaman Asset | LoadAssetPeminjamanOptions |
| `/asset_detail_peminjaman` | Detail Peminjaman Asset | LoadAssetPeminjamanDetail(id) |
| `/asset_detail_pengembalian` | Detail Pengembalian Asset (`isPengembalian`) | LoadAssetPengembalianDetail(id) |
| `/asset_admin_peminjaman` | Peminjaman Asset (admin; argumen `AssetPeminjamanPageMode`, default peminjaman) | LoadAssetPeminjamanAdminList / LoadAssetPengembalianAdminList sesuai mode |
| `/asset_admin_detail_peminjaman` | Detail Peminjaman (admin) | LoadAssetPeminjamanAdminDetail(id) |
| `/asset_admin_detail_pengembalian` | Detail Pengembalian (admin, `isPengembalian`) | LoadAssetPengembalianAdminDetail(id) |

---

## 26. Mobile Screen

### 26.1 Peminjaman Asset (Karyawan) — 4 tab (v1.6)
Tab utama (`AssetPeminjamanTabBar`, tanpa badge): **Permintaan Peminjaman**, **Sedang Dipinjam**, **Pengembalian**, **Riwayat Peminjaman**. Enum `AssetPeminjamanPageMode` kini `{peminjaman, sedangDipinjam, pengembalian, riwayat}` (sebelumnya hanya peminjaman/pengembalian). Lebar tiap tab = lebar layar / 4 dengan minimum 88 dp; bila tidak muat (mis. 320 px) bar dapat digulir horizontal dan tab terakhir tetap terjangkau.

| Tab | Chip | Sumber API |
|---|---|---|
| Permintaan Peminjaman | Pengajuan, Dikonfirmasi, Ditolak, Dibatalkan | `GET …/peminjaman?tab=<chip>`; chip dari `filters` API tanpa `aktif` dan `riwayat` (fallback enum lokal bila `filters` kosong) |
| Sedang Dipinjam | Semua, Jatuh Tempo, Terlambat | `tab=aktif` selalu dikirim; `kondisi=jatuh_tempo`/`terlambat`, `Semua` tidak mengirim `kondisi` |
| Pengembalian | Pengajuan, Selesai (tidak berubah) | `GET …/pengembalian?tab=` |
| Riwayat Peminjaman | – | `tab=riwayat` |

Daftar peminjaman di Bloc dipakai bersama tab Permintaan, Sedang Dipinjam, dan Riwayat; `_listMatchesMode` mencegah data tab lain bocor (mis. daftar aktif tidak tampil di tab Permintaan) dan menampilkan loading sampai daftar tab yang benar dimuat. Pemanggil lama dipetakan oleh `_assetPeminjamanModeFrom` (router): argumen enum dipakai apa adanya, `true` → Pengembalian, lainnya → Permintaan Peminjaman. `NavigateToAssetPeminjaman` menerima `mode:` atau `pengembalian: true`; menu Peminjaman di Asset Saya membuka Permintaan Peminjaman.

Kartu Permintaan/Sedang Dipinjam/Pengembalian: nomor, Tanggal Pengajuan / **Tanggal Diterima** (bila aktif), status (Menunggu Konfirmasi, Dikonfirmasi, Ditolak, **Dibatalkan**, Aktif, **Terlambat**), Keperluan, Tanggal Pinjam, Tanggal Kembali, "{jumlah} Asset", info jadwal serah terima, peringatan "Sudah lewat tanggal pengembalian. Silakan kembalikan asset sekarang." Tombol **+**.

**Kartu Riwayat** (`AssetPeminjamanRiwayatCard`): nomor transaksi, periode (Tanggal Pinjam / Tanggal Kembali), "{jumlah} Asset", badge **Selesai**, dan ringkasan pengembalian: satu pengembalian → "Dikembalikan {tgl} · N normal · M rusak"; lebih dari satu → dijumlahkan dengan tanggal terakhir dan jumlah transaksi ("Dikembalikan terakhir {tgl} · {n} pengembalian · …"). Bila peminjaman tidak punya `pengembalian`, ringkasan tidak ditampilkan; bila tanggalnya kosong tertulis "Dikembalikan -" (RISK-013). Kunci lokalisasi baru di `assets/{id,en}/asset.json`: `menu.permintaan_peminjaman`, `menu.sedang_dipinjam`, `menu.riwayat_peminjaman`, `label.ringkasan_pengembalian(_banyak)`, `pesan.belum_ada_riwayat_peminjaman`, `tab.dibatalkan`, `status.dibatalkan`, serta `tab.semua/jatuh_tempo/terlambat` untuk chip kondisi.

### 26.2 Tambah Peminjaman Asset
| Field UI | Widget | Required | Source |
|---|---|:---:|---|
| Pilih Asset → Tambah Asset | Bottom sheet "Tambah Pinjam Asset" (cari nama/kode/merek) | Ya, min 1 | `options.assets` |
| Form Peminjaman: Pilih Keperluan | Bottom sheet | Ya | `options.keperluan` |
| Tujuan Peminjaman | Text field | Ya | Input |
| Tanggal Pinjam – Tanggal Kembali | Date range picker | Ya, kembali ≥ pinjam | Input |
| Tombol | **Ajukan Peminjaman** | – | – |

### 26.3 Detail Peminjaman / Pengembalian (Karyawan)
Header (Tanggal Pengajuan/Diterima, status, Nomor Pengajuan), Data Karyawan (Jabatan, Cabang, Departemen), Detail Peminjaman (Tanggal Pinjam, Tanggal Kembali, Keperluan, Tujuan Peminjaman), Jadwal Serah Terima, daftar asset dengan status **Disetujui Admin**/**Ditolak Admin** (+Alasan Penolakan) atau kondisi **Normal**/**Rusak** (+Keterangan Kerusakan) untuk pengembalian.
- Status `pengajuan` (v1.6): hanya tombol **Batalkan** (`_EmployeeCancelAction`, dialog "Peminjaman yang dibatalkan tidak dapat dipulihkan.").
- Status `dikonfirmasi`: panel "Siap untuk meminjam Asset?" + instruksi, tombol **Batalkan** dan **Terima Asset** (dialog konfirmasi).
- Status `dibatalkan` (v1.6): badge **Dibatalkan** (abu-abu), tanpa tombol aksi.
- Detail peminjaman memuat bagian **Pengembalian** (`AssetPeminjamanPengembalianSection`): satu kartu per transaksi pengembalian (nomor, status, tanggal, kondisi Normal/Rusak + keterangan kerusakan + lokasi tujuan per asset). Bagian disembunyikan bila `pengembalian` kosong atau pada detail pengembalian.
- `dapat_dikembalikan`: tombol **Kembalikan Asset** (dialog "Asset aktif pada peminjaman ini akan diajukan untuk dikembalikan.").

**Alur status**: `AssetAlurStepper.peminjaman` (Pengajuan → Konfirmasi → Dipinjam → Pengembalian → Selesai; transaksi pengembalian mulai dari langkah Pengembalian; disembunyikan bila ditolak) dan satu kartu timeline per aset (`AssetPeminjamanTimelineCards`). Transaksi peminjaman: Pengajuan (keperluan, periode) · Konfirmasi (jadwal serah terima; chip **Ditolak** per aset) · Dipinjam (tanggal diterima, batas kembali; chip **Terlambat**) · Pengembalian · Selesai (lokasi gudang). Transaksi pengembalian: Pengembalian (jadwal) · Konfirmasi (kondisi Normal/Rusak; chip **Rusak**) · Selesai (lokasi gudang). Alasan penolakan tetap di kartu aset. File: `Shared/Widgets/AssetAlurWidgets.dart`, `Shared/Widgets/AssetPeminjamanTimeline.dart`.

### 26.4 Peminjaman Asset (Admin)
Empat tab dan chip sama seperti karyawan (§26.1; `kondisi` dikirim ke `GET /asset/peminjaman-karyawan`). "Lihat semua" pada panel jatuh tempo di Asset Karyawan membuka tab **Sedang Dipinjam** (`NavigateToAssetAdminPeminjaman(mode: sedangDipinjam)`); aksi Peminjaman dan panel "perlu diproses" membuka Permintaan Peminjaman. Kartu menampilkan karyawan, keperluan, periode, jumlah asset; tab Riwayat memakai `AssetPeminjamanRiwayatCard(showKaryawan: true)` (+ nama/NIK karyawan).

### 26.5 Detail Peminjaman (Admin)
Data karyawan (Jabatan, Cabang, Departemen), detail peminjaman, daftar asset. Status `pengajuan`: tiap asset tombol **Setujui**/**Tolak** (+Alasan Penolakan) dan tombol bawah **Konfirmasi** → sheet **Konfirmasi Peminjaman Asset** (ringkasan, Jadwal Serah Terima dibatasi periode pinjam, Jam Serah Terima, Catatan Tambahan, **Submit**). Jadwal hanya diminta bila ada asset disetujui. Admin **read-only** pada peminjaman `selesai` (v1.6: kondisi `editable` kini memeriksa `!isSelesai`, sehingga form keputusan tidak muncul), label dan warna status **Selesai** benar di detail admin dan karyawan, serta bagian **Pengembalian** ikut tampil.

Stepper dan kartu timeline sama seperti §26.3; kartu timeline disembunyikan saat form keputusan admin terbuka.

### 26.6 Detail Pengembalian (Admin)
Status `pengajuan`: tiap asset **Normal**/**Rusak** (+Keterangan Kerusakan) dropdown **Lokasi Tujuan Asset** per asset (v1.1; default gudang di cabang karyawan) dan tombol **Konfirmasi** (langsung dikirim, tanpa sheet). Bila belum ada pilihan lokasi: "Pilih lokasi gudang tujuan untuk seluruh asset.". Setelah selesai: kondisi dan lokasi tujuan read-only.

Stepper dan kartu timeline transaksi pengembalian sama seperti §26.3; kartu timeline disembunyikan saat form kondisi dan lokasi admin terbuka.

### 26.7 Asset Saya
Banner per peminjaman `dikonfirmasi` (aksi terima), banner jatuh tempo (label dari `sisa_hari`), bagian **Peminjaman Asset** berisi kartu peminjaman aktif.

---

## 27. Mobile → API

| UI Field | Mobile Variable | JSON Field | Type | Required |
|---|---|---|---|:---:|
| Pilih Keperluan | `_selectedPurpose.value` | `keperluan` | string | Ya |
| Tujuan Peminjaman | `_detailController.text.trim()` | `tujuan_peminjaman` | string | Ya |
| Tanggal Pinjam | `tanggalPeminjamanApi(_borrowDate)` | `tgl_pinjam` | Y-m-d | Ya |
| Tanggal Kembali | `tanggalPeminjamanApi(_returnDate)` | `tgl_kembali` | Y-m-d | Ya |
| Tambah Asset | `_selectedAssets[].idAssetData` | `asset_ids[]` | array | Ya |
| Setujui / Tolak (admin) | `AssetPeminjamanAdminKeputusan.statusItem` | `items[].status_item` | `disetujui`/`ditolak` | Ya |
| Alasan Penolakan | `ketTolak.trim()` | `items[].ket_tolak` | string | Bila ditolak |
| Jadwal & Jam Serah Terima | sheet `_handoverDate`, `_handoverTime` | `tgl_serah_terima`, `jam_serah_terima` | Y-m-d, H:i | Bila ada disetujui |
| Catatan Tambahan | sheet | `keterangan_serah_terima` | string | Tidak |
| Normal / Rusak (admin pengembalian) | `AssetPengembalianAdminKondisi.kondisi` | `items[].kondisi` | string | Ya |
| Lokasi Tujuan Asset (admin pengembalian) | `AssetPengembalianAdminKondisi.idLokasi` | `items[].id_asset_lokasi_m` | string | Ya (v1.1) |
| Keterangan Kerusakan | `keteranganKerusakan.trim()` | `items[].keterangan_kerusakan` | string | Bila rusak |
| Chip Permintaan Peminjaman | `_permintaanTab` / `activePeminjamanTab` | query `tab` | string | Ya |
| Chip Sedang Dipinjam | `AssetPeminjamanKondisi.apiValue` | query `tab=aktif` + `kondisi` (`semua` tidak dikirim) | string | Tidak |
| Tab Riwayat | `_tabRiwayat` | query `tab=riwayat` | string | Ya |
| Terima / Batalkan / Kembalikan | – | (tanpa body) | – | – |

---

## 28. API → Mobile

| API JSON | Mobile Model (`AssetPeminjamanRecord`) | UI |
|---|---|---|
| `id`, `no_trx` | id, noTrx | Navigasi, "Nomor Pengajuan" |
| `tgl_pengajuan` / `tgl_diterima` | tanggalPengajuan / tanggalDiterima | "Tanggal Pengajuan" / "Tanggal Diterima" |
| `keperluan`, `tujuan_peminjaman` | keperluan, tujuanPeminjaman | "Keperluan", "Tujuan Peminjaman" |
| `tgl_pinjam`, `tgl_kembali` | tanggalPinjam, tanggalKembali | "Tanggal Pinjam", "Tanggal Kembali" |
| `tgl_serah_terima`, `jam_serah_terima`, `keterangan_serah_terima` | idem | Info "Jadwal serah terima asset pada {tanggal}, jam {jam}." |
| `ket_tolak` | ketTolak | Alasan |
| `status_approval` (+`jenis_status`) | statusApproval (`parseAssetPeminjamanApproval`) | Badge, tombol |
| `status_label`, `tab` | statusLabel, tab | Badge |
| `terlambat` | terlambat | "Terlambat" + peringatan |
| `dapat_dikembalikan` | dapatDikembalikan | Tombol Kembalikan Asset |
| `jenis_status`, `id_peminjaman_asal` | jenisStatus, idPeminjamanAsal | Mode pengembalian |
| `karyawan.*` (termasuk `karyawan.jabatan` ← `master_jabatan.nama`) | karyawan (`jabatan` default `''`) | Data Karyawan: Jabatan, Cabang (`cabang`), Departemen (`divisi`) |
| `assets[]` | assets (`AssetPeminjamanAsset`) | Daftar asset, keputusan/kondisi |
| `lokasi_gudang[]`, `karyawan.id_cabang`, `assets[].nama_lokasi_tujuan` | AssetPeminjamanRecord.lokasiGudang, AssetPeminjamanKaryawan.idCabang, AssetPeminjamanAsset | Dropdown Lokasi Tujuan Asset; lokasi tujuan di kartu |
| `pengembalian[]` (list riwayat: `id,no_trx,tgl_selesai,jumlah_normal,jumlah_rusak`; detail: + `status_approval,status_label,tgl_pengajuan,assets[]`) | AssetPeminjamanRecord.pengembalian (`AssetPeminjamanPengembalian`, `AssetPeminjamanPengembalianAsset`) | Kartu Riwayat (ringkasan), bagian Pengembalian di detail |
| `status_approval = dibatalkan` | `AssetPeminjamanApproval.dibatalkan` | Badge Dibatalkan, chip Dibatalkan |
| `filters`, `records`, `pagination` | AssetPeminjamanPage | Chip, list, infinite scroll |

---

## 29. Mapping Mobile → API → Database

| UI | Mobile Variable | JSON | Backend | Table | Column |
|---|---|---|---|---|---|
| Pilih Keperluan | `keperluan` | `keperluan` | trim | header | keperluan |
| Tujuan Peminjaman | `tujuanPeminjaman` | `tujuan_peminjaman` | trim | header | tujuan_peminjaman |
| Tanggal Pinjam | `tanggalPinjam` | `tgl_pinjam` | `$tanggalPinjam` | header | tgl_peminjaman |
| Tanggal Kembali | `tanggalKembali` | `tgl_kembali` | `$tanggalKembali` | header | tgl_dikembalikan |
| Tambah Asset | `assetIds` | `asset_ids[]` | `$assets` | item / asset_data | id_asset_data (+snapshot) / status_asset = proses |
| (JWT karyawan) | – | Authorization | `$karyawan` | header | id_karyawan, nik_karyawan, nama_karyawan, user_input |
| Setujui/Tolak | `statusItem` | `items[].status_item` | `$statusItem` | item | status_item |
| Alasan Penolakan | `ketTolak` | `items[].ket_tolak` | `$ketTolak` | item | ket_tolak |
| Jadwal | `tanggalSerahTerima`, `jamSerahTerima` | `tgl_serah_terima`, `jam_serah_terima` | Carbon | header | tgl_serah_terima, jam_serah_terima (H:i:s) |
| Catatan Tambahan | `keteranganSerahTerima` | `keterangan_serah_terima` | trim | header | keterangan_serah_terima |
| Tab / chip peminjaman | `activePeminjamanTab` | query `tab` | `applyPeminjamanTabFilter` | header | status |
| Chip Jatuh Tempo / Terlambat | `activePeminjamanKondisi` | query `kondisi` (hanya `tab=aktif`) | `applyKondisiAktifFilter` | header | tgl_dikembalikan |
| Batalkan | – | – | `batalkanKaryawan` | asset_data / header | tersedia (proses+gudang) / status = dibatalkan, ket_tolak |
| Terima Asset | – | – | `terimaKaryawan` | asset_data / header / asset_trx_log | dipinjam, pemegang karyawan / aktif / peminjaman_karyawan |
| Kembalikan Asset | – | – | `ajukanPengembalianKaryawan` | header (baru) | jenis_status = pengembalian, id_peminjaman_asal |
| Normal/Rusak | `kondisi` | `items[].kondisi` | `$condition` | item / asset_data | status_item (disetujui/ditolak) / status_asset (tersedia/rusak) |
| Keterangan Kerusakan | `keteranganKerusakan` | `items[].keterangan_kerusakan` | `$damageNote` | item / asset_data | ket_tolak / ket_rusak |
| Lokasi Tujuan Asset | `idLokasi` | `items[].id_asset_lokasi_m` | `resolveItemLocations` | item / asset_data | id_asset_lokasi_m_tujuan / id_asset_lokasi_m |

---

## 30. Mapping Database → API → Mobile

| Table | Column | Backend | API JSON | Mobile | UI |
|---|---|---|---|---|---|
| header | tgl_input | dateOnly | `tgl_pengajuan` | tanggalPengajuan | Tanggal Pengajuan |
| header | tgl_update (saat aktif) | dateOnly | `tgl_diterima` | tanggalDiterima | Tanggal Diterima |
| header | tgl_peminjaman, tgl_dikembalikan | – | `tgl_pinjam`, `tgl_kembali` | tanggalPinjam, tanggalKembali | Periode |
| header | tgl_dikembalikan + status | isTerlambat | `terlambat` | terlambat | Terlambat |
| header | status + jenis_status | peminjamanStatusMetadata | `status_approval`, `status_label`, `tab` | statusApproval, statusLabel, tab | Badge, tab |
| header + item + asset_data | – | getDetail | `dapat_dikembalikan` | dapatDikembalikan | Tombol Kembalikan |
| item | COUNT | peminjamanQuery | `jumlah_asset` | jumlahAsset | "{jumlah} Asset" |
| item | status_item (pengembalian dipetakan) | getDetail | `assets[].status_item` | status | Disetujui/Ditolak Admin, Normal/Rusak |
| item | ket_tolak | getDetail | `assets[].ket_tolak` | ketTolak | Alasan / Keterangan Kerusakan |
| asset_data | kode_asset, merek | LEFT JOIN | `kode_asset`, `merek` | – | Kartu asset |
| header (jatuh tempo) | tgl_dikembalikan | getJatuhTempo | `sisa_hari` | sisaHari | Banner jatuh tempo |
| header (pengembalian selesai) + item | tgl_approval, COUNT status_item | attachRiwayatPengembalian | `pengembalian[].tgl_selesai`, `jumlah_normal`, `jumlah_rusak` | AssetPeminjamanPengembalian | Ringkasan kartu Riwayat |
| header (pengembalian) + item + asset_lokasi_m | status, tgl_input, status_item, ket_tolak, nama_lokasi | pengembalianForDetail | `pengembalian[]` (detail) | AssetPeminjamanPengembalian(+Asset) | Bagian Pengembalian di detail |
| header | status = dibatalkan | peminjamanStatusMetadata | `status_approval`, `status_label = Dibatalkan`, `tab = dibatalkan` | AssetPeminjamanApproval.dibatalkan | Badge Dibatalkan |

---

## 31. Validation

### 31.1 Mobile
| Lokasi | Aturan | Pesan |
|---|---|---|
| Form | Min 1 asset | "Tambahkan minimal satu asset." |
| Form | Keperluan wajib | "Keperluan wajib dipilih." |
| Form | Tujuan wajib (trim) | "Tujuan peminjaman wajib diisi." |
| Form | Tanggal pinjam & kembali wajib; kembali ≥ pinjam | "Tanggal pinjam wajib dipilih." / "Tanggal kembali wajib dipilih." / "Tanggal kembali tidak boleh sebelum tanggal pinjam." |
| Form | Picker menyembunyikan asset yang sudah dipilih | – |
| Detail admin | Semua asset diputuskan; tolak wajib alasan | "Pilih keputusan untuk seluruh asset." / "Alasan penolakan wajib diisi." |
| Sheet admin | Tanggal & jam wajib bila ada disetujui; tanggal dibatasi periode pinjam | "Tanggal serah terima wajib dipilih." / "Jam serah terima wajib dipilih." |
| Detail pengembalian admin | Semua kondisi diisi; rusak wajib keterangan | "Pilih kondisi untuk seluruh asset." / "Keterangan kerusakan wajib diisi." |
| Detail pengembalian admin | Setiap asset wajib punya lokasi gudang (v1.1) | "Pilih lokasi gudang tujuan untuk seluruh asset." |
| Terima/Batalkan/Kembalikan | Dialog konfirmasi | – |
| API list | `tab` ∈ pengajuan, dikonfirmasi, ditolak, aktif, dibatalkan, riwayat; `kondisi` ∈ semua, jatuh_tempo, terlambat (v1.6) | "Filter peminjaman asset tidak valid." / "Filter kondisi peminjaman asset tidak valid." |

### 31.2 Backend — lihat §9 dan §24.

### 31.3 Database
ENUM `jenis_status`, `status` (membutuhkan nilai `dibatalkan` dari SQL v1.6; pada MariaDB non-strict nilai enum tak dikenal tersimpan kosong, RISK-010), `status_item`; NOT NULL + default untuk tanggal (`CURRENT_DATE`, `0000-00-00`) dan teks; `no_trx` tidak unique.

### 31.4 Ringkasan per Field
| Field | Mobile | Backend | Database |
|---|---|---|---|
| keperluan | Wajib, dari opsi | required ≤255 (tidak dicek terhadap opsi) | varchar(255) |
| tujuan_peminjaman | Wajib (tanpa batas panjang) | required ≤255 | varchar(255) |
| tgl_pinjam | Wajib | ≥ hari ini | date |
| tgl_kembali | Wajib, ≥ pinjam | ≥ tgl_pinjam | date |
| asset_ids | Min 1, tanpa duplikat | tersedia @gudang, tidak aktif | item |
| status_item (admin) | Semua dipilih | in disetujui/ditolak, lengkap | enum |
| jadwal serah terima | Dalam periode | wajib bila ada disetujui, dalam periode | date/time |
| kondisi (pengembalian) | Semua dipilih | in normal/rusak, lengkap | enum (disetujui/ditolak) |

---

## 32. Error Handling

| Error | HTTP | Backend | Mobile |
|---|---:|---|---|
| Field tidak valid | 422 | VALIDATION_ERROR + errors | Error field / snackbar |
| Aturan bisnis (termasuk Batalkan pada status tidak valid) | 422 | BUSINESS_RULE_VIOLATION | Snackbar error |
| Parameter list `tab`/`kondisi` tidak valid | 422 | VALIDATION_ERROR: "Parameter daftar peminjaman asset tidak valid.", `errors.tab` "Filter peminjaman asset tidak valid.", `errors.kondisi` "Filter kondisi peminjaman asset tidak valid." (EN: "Invalid asset loan filter." / "Invalid asset loan condition filter.") | Snackbar / halaman error + Coba Lagi |
| Detail tidak ditemukan | 404 | NOT_FOUND | Halaman error + Coba Lagi |
| Token | 401 | UNAUTHORIZED | Refresh & retry |
| Bukan admin | 403 | FORBIDDEN | Pesan error |
| Exception | 500 | SYSTEM_ERROR + log | Snackbar |
| Timeout / koneksi | – | – | "Tidak dapat terhubung ke server. Periksa koneksi Anda." |
| Double submit | – | tanpa idempotency | Bloc abaikan saat submitting; tombol loading |
| Notifikasi gagal | – | di-log | – |

---

## 33. Audit Trail

| Data | Disimpan? | Lokasi |
|---|:---:|---|
| Pengaju & waktu | ✅ | `user_input`, `tgl_input` |
| Admin pemutus & waktu | ✅ | `user_approval`, `tgl_approval` |
| Keputusan per asset + alasan | ✅ | item `status_item`, `ket_tolak` |
| Waktu diterima karyawan | ⚠️ tidak langsung | `tgl_update` saat `aktif` (tertimpa bila ada update lain) |
| Pembatalan karyawan | ✅ (v1.6) | status `dibatalkan` + `ket_tolak` teks, `user_update`, `tgl_update` (data sebelum v1.6: `ditolak` + teks yang sama) |
| Serah terima fisik | ✅ | `asset_trx_log peminjaman_karyawan` |
| Pengembalian + kondisi | ✅ | item pengembalian + `asset_trx_log pengembalian_karyawan` |
| Histori status | ❌ | ⚠️ Belum ditemukan |

---

## 34. Notification

| Event | Trigger | Penerima | Judul | Pesan | Rute mobile |
|---|---|---|---|---|---|
| `peminjaman.diajukan` | Ajukan | Semua admin | Pengajuan Peminjaman Asset | {nama} mengajukan peminjaman asset {no_trx}. | `asset_admin_detail_peminjaman` |
| `peminjaman.disetujui` | Konfirmasi (≥1 disetujui) | Karyawan | Peminjaman Asset Disetujui | Peminjaman asset {no_trx} telah dikonfirmasi admin. | `asset_detail_peminjaman` |
| `peminjaman.ditolak` | Konfirmasi (semua ditolak) | Karyawan | Peminjaman Asset Ditolak | Peminjaman asset {no_trx} ditolak admin. | `asset_detail_peminjaman` |
| `peminjaman.diterima` | Terima Asset | Semua admin | Asset Peminjaman Diterima | {nama} telah menerima asset peminjaman {no_trx}. | `asset_admin_detail_peminjaman` |
| `peminjaman.dibatalkan` | Batalkan (status pengajuan atau dikonfirmasi) | Semua admin | Peminjaman Asset Dibatalkan | {nama} membatalkan peminjaman asset {no_trx}. | `asset_admin_detail_peminjaman` |
| `pengembalian.diajukan` | Kembalikan Asset | Semua admin | Pengajuan Pengembalian Asset | {nama} mengajukan pengembalian asset {no_trx}. | `asset_admin_detail_pengembalian` |
| `pengembalian.dikonfirmasi` | Konfirmasi pengembalian | Karyawan | Pengembalian Asset Dikonfirmasi | Pengembalian asset {no_trx} telah dikonfirmasi admin. | `asset_detail_pengembalian` |

Pengingat jatuh tempo **bukan** notifikasi push: hanya daftar/banner (`getJatuhTempo`). ⚠️ Belum ditemukan: scheduler (`Kernel::schedule` kosong).

---

## 35. Security

| Aspek | Implementasi |
|---|---|
| Autentikasi | Bearer JWT; context dari token |
| Isolasi company | Semua query `id_company = ctx.id_company` |
| Otorisasi admin | `accessError` + `findAssetAdmin` |
| Kepemilikan | Endpoint karyawan memfilter `id_karyawan = ctx.id_karyawan`; pengajuan selalu atas nama user login. Batalkan hanya untuk header `jenis_status = peminjaman` milik pemanggil; baris pengembalian atau milik orang lain → "Peminjaman asset tidak ditemukan." Tab Riwayat dan `pengembalian[]` hanya menempelkan pengembalian dengan `id_karyawan` dan company yang sama dengan peminjaman |
| Integritas asset | Lock company + `lockForUpdate`; cek status & pemegang asset di setiap langkah |
| Pemisahan offboarding | Pengembalian offboarding tidak bisa diproses di menu peminjaman |
| Catatan | Validasi request dijalankan sebelum cek admin (konfirmasi peminjaman/pengembalian) |

---

## 36. Manual Book User/Admin

> Label dari `assets/id/asset.json`. ⚠️ Asumsi: halaman admin **Asset Karyawan** diakses dari **Asset Management**. Screenshot menyusul.

### A. Karyawan — Meminjam Asset
1. Buka **Asset Saya** → menu **Peminjaman** → tekan **+**.
2. **Pilih Asset** → **Tambah Asset** → cari dan pilih asset → **Pilih**. Ulangi bila perlu.
3. Di **Form Peminjaman**: **Pilih Keperluan** (Kantor/Dinas/Pelatihan/Lainnya), isi **Tujuan Peminjaman**, pilih **Tanggal Pinjam** dan **Tanggal Kembali**.
4. Tekan **Ajukan Peminjaman**. Status **Menunggu Konfirmasi**.
5. Salah mengajukan? Selama masih **Menunggu Konfirmasi**, buka detail transaksi (tab **Permintaan Peminjaman**, chip **Pengajuan**) → **Batalkan** → konfirmasi. Status menjadi **Dibatalkan** (chip **Dibatalkan**) dan asset kembali tersedia.

### B. Admin — Menyetujui Peminjaman
1. **Asset Karyawan** → aksi **Peminjaman** → tab **Permintaan Peminjaman** → chip **Pengajuan** → pilih transaksi.
2. Untuk setiap asset pilih **Setujui** atau **Tolak** (isi **Alasan Penolakan**).
3. Tekan **Konfirmasi** → isi **Jadwal Serah Terima**, **Jam Serah Terima** (dalam periode pinjam) dan **Catatan Tambahan** → **Submit**.

### C. Karyawan — Menerima Asset
1. Buka notifikasi **Peminjaman Asset Disetujui** atau banner di Asset Saya.
2. Datang sesuai jadwal serah terima, periksa asset secara fisik.
3. Tekan **Terima Asset** → konfirmasi. Status **Aktif**.
   - Tidak jadi meminjam: tekan **Batalkan** (tidak dapat dipulihkan). Status menjadi **Dibatalkan**, bukan Ditolak.

### D. Karyawan — Mengembalikan Asset
1. Perhatikan pengingat jatuh tempo di Asset Saya; lewat tanggal akan tertulis **Terlambat**. Tab **Sedang Dipinjam** dengan chip **Jatuh Tempo** / **Terlambat** menyaring peminjaman aktif.
2. Buka detail peminjaman aktif → **Kembalikan Asset** → konfirmasi.
3. Serahkan asset ke admin; pantau di tab **Pengembalian**. Setelah admin mengonfirmasi, peminjaman tampil di tab **Riwayat Peminjaman** beserta ringkasan pengembaliannya (mis. "Dikembalikan 28 Okt 2026 · 0 normal · 1 rusak").

### E. Admin — Menerima Pengembalian
1. **Peminjaman** → tab **Pengembalian** → chip **Pengajuan** → pilih transaksi.
2. Periksa setiap asset: **Normal** atau **Rusak** (isi **Keterangan Kerusakan**).
3. Pilih **Lokasi Tujuan Asset** (gudang) untuk setiap asset; default-nya gudang di cabang karyawan.
4. Tekan **Konfirmasi**. Asset normal kembali tersedia, asset rusak berstatus rusak, keduanya di gudang yang dipilih; peminjaman otomatis selesai.

### F. Melihat Riwayat Peminjaman (Karyawan & Admin)
1. Buka tab **Riwayat Peminjaman**. Daftar berisi peminjaman yang sudah selesai (terbaru di atas); admin melihat seluruh karyawan di company, karyawan hanya miliknya.
2. Kartu menampilkan nomor, periode, jumlah asset, badge **Selesai**, dan ringkasan pengembalian. Pengembalian yang berasal dari proses offboarding juga ikut dihitung di sini.
3. Buka kartu untuk melihat detail; bagian **Pengembalian** merinci kondisi tiap asset. Detail selesai bersifat baca-saja bagi admin.

---

## 37. Kondisi Button

| Button | Role | Halaman | Condition | Action |
|---|---|---|---|---|
| + | Karyawan | Peminjaman Asset | Selalu | Form peminjaman |
| Ajukan Peminjaman | Karyawan | Form | Tidak submitting | POST peminjaman |
| Batalkan | Karyawan | Detail | Status `pengajuan` (v1.6; tombol tunggal) | Dialog → POST batalkan |
| Terima Asset / Batalkan | Karyawan | Detail | Status `dikonfirmasi` | Dialog → POST terima / batalkan |
| Tab Permintaan / Sedang Dipinjam / Pengembalian / Riwayat | Karyawan, Admin | Peminjaman Asset | Selalu (v1.6; tanpa badge) | Ganti daftar; bar dapat digulir di layar sempit |
| Chip Pengajuan / Dikonfirmasi / Ditolak / Dibatalkan | Karyawan, Admin | Tab Permintaan Peminjaman | Selalu | `tab=<chip>` |
| Chip Semua / Jatuh Tempo / Terlambat | Karyawan, Admin | Tab Sedang Dipinjam | Selalu | `tab=aktif` (+ `kondisi`) |
| Kembalikan Asset | Karyawan | Detail | `dapat_dikembalikan = true` | Dialog → POST pengembalian |
| Setujui / Tolak per asset | Admin | Detail peminjaman | Tab `pengajuan` | Set keputusan |
| Konfirmasi | Admin | Detail peminjaman | Tab `pengajuan` | Sheet jadwal → POST konfirmasi |
| Submit | Admin | Sheet | Jadwal valid bila ada disetujui | Kirim |
| Normal / Rusak per asset | Admin | Detail pengembalian | Tab `pengajuan` | Set kondisi |
| Lokasi Tujuan Asset per asset | Admin | Detail pengembalian | Tab `pengajuan` | Pilih gudang (wajib, v1.1) |
| Konfirmasi | Admin | Detail pengembalian | Tab `pengajuan` | POST konfirmasi pengembalian |
| Tolak pengembalian / Perpanjang | – | – | ⚠️ Belum ditemukan | – |

---

## 38. Testing Scenario

### Happy Path
| TC | Scenario | Steps | Expected |
|---|---|---|---|
| TC-01 | Ajukan peminjaman | Isi form lengkap → Ajukan | 201; `pengajuan`; asset `proses`; notif admin |
| TC-02 | Setujui semua | Admin Setujui semua + jadwal dalam periode | `dikonfirmasi`; jadwal tersimpan; notif karyawan |
| TC-03 | Setujui sebagian | 1 setujui, 1 tolak + alasan | `dikonfirmasi`; asset ditolak `tersedia`; item ditolak tampil "Ditolak Admin" |
| TC-04 | Tolak semua | Semua tolak | `ditolak`, `ket_tolak` "Seluruh asset peminjaman ditolak."; asset `tersedia` |
| TC-05 | Terima asset | Karyawan Terima Asset | `aktif`; asset `dipinjam` @karyawan; `asset_trx_log peminjaman_karyawan`; notif admin |
| TC-06 | Batalkan saat dikonfirmasi | Karyawan Batalkan saat dikonfirmasi | `dibatalkan` (bukan `ditolak`), "Dibatalkan oleh karyawan."; asset disetujui `tersedia`; tidak ada log |
| TC-07 | Ajukan pengembalian | Kembalikan Asset | Header pengembalian `pengajuan` dengan `id_peminjaman_asal`; notif admin |
| TC-08 | Pengembalian normal | Admin Normal → Konfirmasi | Asset `tersedia`; log; pengembalian & peminjaman asal `selesai` |
| TC-09 | Pengembalian rusak | Rusak + keterangan | Asset `rusak`, `ket_rusak`; log kondisi_akhir rusak |

### Validation
| TC | Scenario | Expected |
|---|---|---|
| TC-10 | Tanpa asset | "Tambahkan minimal satu asset." / 422 |
| TC-11 | Tanpa keperluan / tujuan | 422 |
| TC-12 | Tanggal pinjam kemarin (API) | 422 "Tanggal pinjam tidak boleh sebelum hari ini." |
| TC-13 | Tanggal kembali < pinjam | 422 |
| TC-14 | Asset tidak tersedia | 422 "…tidak tersedia untuk dipinjam." |
| TC-15 | Admin tidak memutuskan semua asset | 422 "Seluruh asset peminjaman wajib dikonfirmasi." |
| TC-16 | Tolak tanpa alasan | 422 |
| TC-17 | Jadwal di luar periode | 422 "Tanggal serah terima harus berada dalam periode peminjaman." |
| TC-18 | Ada disetujui tanpa jadwal | 422 "Tanggal serah terima wajib diisi." |
| TC-19 | Pengembalian tanpa semua kondisi | 422 |
| TC-20 | Rusak tanpa keterangan | 422 |

### Status
| TC | Scenario | Expected |
|---|---|---|
| TC-21 | Konfirmasi ulang peminjaman | 422 "…sudah tidak menunggu konfirmasi." |
| TC-22 | Terima saat masih pengajuan | 422 "Peminjaman asset belum dapat diterima." |
| TC-23 | Batalkan saat aktif / selesai / ditolak / dibatalkan | 422 "…sudah tidak dapat dibatalkan." |
| TC-24 | Kembalikan saat belum aktif | 422 "Hanya peminjaman aktif yang dapat dikembalikan." |
| TC-25 | Ajukan pengembalian dua kali | 422 "Pengembalian asset sedang menunggu konfirmasi." |
| TC-26 | Konfirmasi pengembalian dua kali | 422 |
| TC-27 | Konfirmasi pengembalian milik offboarding | Tidak tampil; API 422 / 404 |

### Asset / Jatuh Tempo
| TC | Scenario | Expected |
|---|---|---|
| TC-28 | Asset sedang dipinjam orang lain | Tidak muncul di opsi; API 422 |
| TC-29 | Asset sedang di onboarding | 422 |
| TC-30 | Tanggal kembali = besok | Muncul di pengingat jatuh tempo (`sisa_hari = 1`) |
| TC-31 | Lewat tanggal kembali | `terlambat = true`; label Terlambat; `sisa_hari` negatif |
| TC-32 | Sudah ajukan pengembalian saat jatuh tempo | Hilang dari pengingat |

### API / Security / Network
| TC | Scenario | Expected |
|---|---|---|
| TC-33 | Tanpa token | 401 |
| TC-34 | Non-admin konfirmasi | 403 (atau 422 bila payload invalid) |
| TC-35 | Karyawan akses peminjaman orang lain | 404 / 422 |
| TC-36 | Double tap Terima | Hanya satu request; request kedua 422 |
| TC-37 | Timeout setelah commit ajukan | Ajukan ulang ditolak (asset `proses`) |
| TC-38 | Konfirmasi pengembalian dengan lokasi gudang berbeda per asset | `asset_data.id_asset_lokasi_m` dan item `id_asset_lokasi_m_tujuan` sesuai pilihan masing-masing; log tercatat; `selesai` |
| TC-39 | Konfirmasi pengembalian tanpa lokasi / lokasi tidak ada / lokasi bertipe ruangan | 422 "Lokasi gudang tujuan setiap asset wajib dipilih." atau "Lokasi gudang tujuan tidak valid."; status tetap `pengajuan` |
| TC-40 | Karyawan Terima Asset pinjaman (asset keluar gudang) | `asset_data.id_asset_lokasi_m` dikosongkan; status `dipinjam` @karyawan |

### Pembatalan (v1.6)
| TC | Scenario | Expected |
|---|---|---|
| TC-41 | Batalkan saat `pengajuan` (sebelum admin memutuskan) | 200 "Peminjaman asset berhasil dibatalkan."; header `dibatalkan`, `ket_tolak` "Dibatalkan oleh karyawan."; semua asset item (status_asset `proses`) kembali `tersedia` @gudang; notif `peminjaman.dibatalkan` ke admin; tidak ada `asset_trx_log` |
| TC-42 | Batalkan saat `dikonfirmasi` dengan 1 disetujui + 1 ditolak | Hanya asset disetujui yang diubah ke `tersedia` (asset ditolak sudah tersedia sejak konfirmasi); header `dibatalkan` |
| TC-43 | Batalkan saat asset item sudah bukan `proses` + gudang | Asset itu tidak disentuh; header tetap `dibatalkan` |
| TC-44 | Batalkan peminjaman karyawan lain, id tidak ada, atau id baris pengembalian | 422 "Peminjaman asset tidak ditemukan."; tidak ada perubahan |
| TC-45 | Konfirmasi admin pada peminjaman yang sudah `dibatalkan` | 422 "Peminjaman asset sudah tidak menunggu konfirmasi." |

### List: tab Dibatalkan, kondisi, Riwayat (v1.6)
| TC | Scenario | Expected |
|---|---|---|
| TC-46 | `tab=dibatalkan` (karyawan dan admin) | Hanya header `dibatalkan`; `filters` memuat Pengajuan, Dikonfirmasi, Ditolak, Aktif, Dibatalkan |
| TC-47 | `tab=aktif&kondisi=terlambat` | Hanya aktif dengan `tgl_kembali` < hari ini |
| TC-48 | `tab=aktif&kondisi=jatuh_tempo` | Hanya aktif dengan `tgl_kembali` hari ini sampai besok; yang terlambat tidak muncul |
| TC-49 | `tab=pengajuan&kondisi=terlambat` | `kondisi` diabaikan; daftar pengajuan biasa |
| TC-50 | `kondisi=xyz` | 422; `errors.kondisi` "Filter kondisi peminjaman asset tidak valid." (EN "Invalid asset loan condition filter.") |
| TC-51 | `kondisi` tanpa `tab`/`limit`/`offset` | Respons array lama (non-paginasi); `kondisi` diabaikan |
| TC-52 | `kondisi` dengan `limit` kecil | Filter diterapkan sebelum paginasi: `has_more` dan `next_offset` konsisten dengan hasil tersaring |
| TC-53 | `tab=riwayat` | Hanya `selesai`, urut `tgl_update` desc lalu id desc; tiap item punya `pengembalian[]` (hanya `selesai`, `id,no_trx,tgl_selesai,jumlah_normal,jumlah_rusak`) |
| TC-54 | `tab=riwayat` dengan `limit` kecil | `limit+1`: `has_more` benar, `next_offset` = offset + jumlah record |
| TC-55 | Pengembalian dari offboarding pada peminjaman selesai | Tampil di tab Riwayat dan detail peminjaman; tidak tampil di tab/list/detail Pengembalian |
| TC-56 | Riwayat karyawan vs admin | Karyawan hanya miliknya; admin seluruh company; tidak lintas company |
| TC-57 | Detail peminjaman | `pengembalian[]` berisi semua status (urut `tgl_input` asc) dengan `assets[].kondisi/keterangan_kerusakan/lokasi_tujuan`; kosong bila belum ada; tidak ada pada detail pengembalian |

### Mobile (v1.6)
| TC | Scenario | Expected |
|---|---|---|
| TC-58 | Tab bar di layar 320 px | Empat tab dapat digulir horizontal dan tab terakhir terjangkau |
| TC-59 | Berpindah Permintaan → Sedang Dipinjam → Riwayat → kembali | Tidak ada data tab lain yang bocor; tab Permintaan kembali ke chip terakhir |
| TC-60 | Sedang Dipinjam: chip Terlambat, lalu Semua | Request `tab=aktif&kondisi=terlambat`; `Semua` mengirim `tab=aktif` tanpa `kondisi`. Tab lain tidak mengirim `kondisi` |
| TC-61 | Kartu Riwayat: 1 pengembalian, >1 pengembalian, tanpa pengembalian | "Dikembalikan {tgl} · N normal · M rusak"; dijumlah + tanggal terakhir + jumlah transaksi; ringkasan disembunyikan |
| TC-62 | Detail karyawan: pengajuan / dibatalkan | Pengajuan: hanya tombol Batalkan (tanpa Terima); Dibatalkan: badge Dibatalkan tanpa aksi |
| TC-63 | Detail admin peminjaman `selesai` | Read-only (tanpa Setujui/Tolak/Konfirmasi), label dan warna Selesai, bagian Pengembalian tampil |

---

## 39. Temuan Ketidaksesuaian

| ID | Component | Finding | Impact | Recommendation |
|---|---|---|---|---|
| INC-01 | DB ↔ Backend | Status pengembalian `ditolak` tidak pernah di-set; `dikonfirmasi` hanya dipakai pengembalian offboarding | Enum membingungkan | Dokumentasikan / implementasi tolak pengembalian |
| INC-02 | DB | Enum item `approval_item` (disetujui/ditolak) dipakai ulang untuk kondisi pengembalian (normal/rusak); `ket_tolak` menyimpan keterangan kerusakan | Makna kolom bergantung jenis baris | Kolom kondisi terpisah |
| INC-03 | Backend | ✅ **Diperbaiki v1.6**: pembatalan karyawan kini status `dibatalkan`. Sisa: data lama masih `ditolak` + teks "Dibatalkan oleh karyawan." | Data lama tidak terbedakan dari penolakan admin kecuali teks; tab Dibatalkan tidak memuatnya | Jalankan SQL langkah 3 (opsional) |
| INC-04 | Backend ↔ Mobile | ✅ **Diperbaiki v1.6**: tab **Riwayat Peminjaman** menampilkan peminjaman `selesai` beserta pengembaliannya | – | – |
| INC-05 | Mobile ↔ Backend | Field Tujuan Peminjaman tanpa batas panjang di mobile; backend maks 255 | Error 422 setelah submit | `maxLength: 255` |
| INC-06 | Backend | `keperluan` tidak divalidasi terhadap daftar opsi | Nilai bebas via API | `in:kantor,dinas,pelatihan,lainnya` |
| INC-07 | Backend | `tgl_diterima` diambil dari `tgl_update` | Bisa bergeser jika header di-update | Kolom khusus |
| INC-08 | Backend | Validasi request sebelum cek admin | 422 vs 403 | Pindahkan cek akses |
| INC-09 | Backend | `konfirmasiAdmin` tidak memeriksa karyawan masih aktif (baru dicek saat terima) | Asset tertahan bila karyawan nonaktif | Cek di konfirmasi |
| INC-10 | Backend ↔ Mobile | Chip **Jatuh Tempo** (`kondisi=jatuh_tempo`) hanya hari ini–besok dan tidak mengecualikan peminjaman yang pengembaliannya sedang diajukan; hitungan jatuh tempo dashboard/banner (`jatuhTempoQuery`) mencakup yang sudah lewat (≤ besok) dan mengecualikan yang pengembaliannya `pengajuan`/`dikonfirmasi` | Jumlah chip dan angka dashboard bisa berbeda | Sepakati definisi tunggal atau beri label |
| INC-11 | Backend ↔ DB | Kode memakai nilai enum `dibatalkan` yang baru ada setelah SQL manual dijalankan; DBML sudah diperbarui, migrasi tidak ada | Urutan deploy menentukan (RISK-010) | Jadikan migration atau cek otomatis sebelum rilis |

---

## 40. Edge Case

| Edge Case | Current Behavior | Impact | Recommendation |
|---|---|---|---|
| Karyawan tidak pernah menekan Terima | Status `dikonfirmasi` selamanya, asset `proses` | Asset terkunci | Kedaluwarsa otomatis / batal oleh admin |
| Karyawan ingin membatalkan sebelum admin memutuskan | ✅ Bisa (v1.6): `batalkan` pada status `pengajuan` → `dibatalkan`, semua asset item dilepas | – | – |
| Karyawan membatalkan bersamaan dengan admin mengonfirmasi | Keduanya memakai lock company dan lock header; yang kedua gagal 422 ("…sudah tidak menunggu konfirmasi." / "…sudah tidak dapat dibatalkan.") | Aman | – |
| Data pembatalan sebelum v1.6 | Tersimpan `ditolak` + "Dibatalkan oleh karyawan."; tidak muncul di chip Dibatalkan | Riwayat pembatalan terbagi dua status | SQL langkah 3 (opsional) |
| Peminjaman dengan pengembalian dari offboarding | Peminjaman `selesai` tampil di Riwayat dan detail memuat pengembalian offboarding; tab/list/detail Pengembalian tetap menyembunyikannya | Keputusan produk | – |
| Pengembalian `selesai` tanpa `tgl_approval` | Kartu Riwayat menampilkan "Dikembalikan -" | Kosmetik | Fallback ke `tgl_update` |
| API v1.6 jalan sebelum SQL enum | Batalkan menulis `dibatalkan` ke kolom enum lama; MariaDB non-strict menyimpan string kosong, mode SQL strict menolak query | Status rusak / 500 | Jalankan SQL dulu (RISK-010) |
| Admin tidak memproses pengembalian | Pengembalian `pengajuan` terus; asset tetap `dipinjam`; pengingat jatuh tempo hilang | Asset lepas dari pantauan | SLA / pengingat admin |
| Pengembalian perlu ditolak (asset belum diserahkan) | Tidak ada aksi tolak | Admin terpaksa konfirmasi | Endpoint tolak |
| Tanggal pinjam sudah lewat saat admin konfirmasi | Jadwal tetap harus di dalam periode (bisa di masa lalu) | Jadwal tidak realistis | Validasi jadwal ≥ hari ini |
| Karyawan nonaktif saat meminjam | Pengembalian masih bisa diajukan & dikonfirmasi; offboarding juga membuat pengembalian | – | – |
| Asset dipinjam lalu karyawan offboarding | Offboarding membuat pengembalian anak (relasi) yang disembunyikan dari menu peminjaman | Aman | – |
| Pengembalian sebagian asset | Tidak didukung (semua asset ikut) | Karyawan harus mengembalikan semua | Pilihan item |
| Perpanjangan peminjaman | Tidak ada | Harus ajukan ulang | Fitur perpanjangan |
| Dua karyawan mengajukan asset sama bersamaan | Lock company; kedua 422 | Aman | – |

---

## 41. Potential Bug / Technical Risk

### RISK-001 — Asset terkunci bila peminjaman tidak pernah diterima (TINGGI)
**Lokasi:** `konfirmasiAdmin`, `terimaKaryawan`, `batalkanKaryawan`.
**Masalah:** Setelah `dikonfirmasi`, hanya karyawan yang bisa menerima/membatalkan. Tidak ada kedaluwarsa atau aksi admin.
**Impact:** Asset disetujui tetap `proses` tanpa batas waktu, tidak bisa dipinjam/diberikan ke orang lain.
**Recommendation:** Pembatalan oleh admin dan/atau kedaluwarsa otomatis (mis. lewat `tgl_dikembalikan`).

### RISK-002 — Pengajuan tidak bisa dibatalkan karyawan (SEDANG) — ✅ DITUTUP v1.6
Karyawan kini dapat membatalkan pada status `pengajuan` dan `dikonfirmasi`; asset yang dikunci dilepas (`proses` → `tersedia`) dan status menjadi `dibatalkan`. Syarat deploy: SQL enum `dibatalkan` (RISK-010).

### RISK-003 — Tidak ada pengingat otomatis (SEDANG)
`Kernel::schedule` kosong; pengingat jatuh tempo hanya muncul ketika aplikasi dibuka. Tidak ada push/notif H-1 atau terlambat.

### RISK-004 — Pengembalian tidak bisa ditolak/dibatalkan (SEDANG)
Pengembalian yang salah diajukan tidak bisa dibatalkan karyawan atau ditolak admin; admin harus mengonfirmasi (asset otomatis masuk gudang).

### RISK-005 — Pengembalian wajib seluruh asset (RENDAH)
Tidak bisa mengembalikan sebagian; desain atau keterbatasan perlu dikonfirmasi bisnis.

### RISK-006 — Tanggal nol `0000-00-00` (SEDANG)
Insert memakai `0000-00-00`/`00:00:00`; ⚠️ Asumsi: gagal pada MySQL strict mode `NO_ZERO_DATE`.

### RISK-007 — Tanpa batas durasi pinjam (RENDAH)
`tgl_kembali` bebas selama ≥ `tgl_pinjam`.

### RISK-008 — Kapasitas penomoran (RENDAH)
3 digit/bulan/company dipakai bersama peminjaman & pengembalian (header) serta item.

### RISK-009 — Notifikasi di luar transaksi (RENDAH)
Kegagalan notif hanya di-log.

### RISK-010 — Urutan deploy: SQL enum harus sebelum API v1.6 (TINGGI)
**Lokasi:** `batalkanKaryawan` menulis `status = 'dibatalkan'`; filter `tab=dibatalkan`.
**Masalah:** Nilai `dibatalkan` hanya ada di kolom setelah `sql-peminjaman-status-dibatalkan.sql` dijalankan. Bila API v1.6 dipakai lebih dulu, pada MariaDB non-strict nilai enum tak dikenal tersimpan sebagai string kosong (`''`), sedangkan mode strict menolak query. Status eksekusi SQL di server tidak dapat diverifikasi dari kode.
**Impact:** Peminjaman yang dibatalkan tidak bisa dikenali statusnya (asset sudah dilepas, header tidak terbaca sebagai `dibatalkan`).
**Recommendation:** Jalankan SQL (langkah 0–2) lebih dulu dan verifikasi `COLUMN_TYPE`; pertimbangkan migration atau pemeriksaan pra-rilis.

### RISK-011 — Data pembatalan lama tetap `ditolak` (RENDAH)
Pembatalan sebelum v1.6 tersimpan `ditolak` + "Dibatalkan oleh karyawan.". Tab Dibatalkan hanya memuat status baru; SQL langkah 3 bersifat opsional.

### RISK-012 — Detail peminjaman menjalankan beberapa query per pengembalian (RENDAH)
`pengembalianForDetail` memanggil `getDetail` per transaksi pengembalian (sedikitnya query header dan item per pengembalian, selain satu query daftar). Jumlah pengembalian per peminjaman umumnya kecil (satu pengembalian menutup seluruh asset), tetapi pola ini N+1 bila bertambah.

### RISK-013 — Kartu Riwayat "Dikembalikan -" (RENDAH)
Bila `tgl_approval` pengembalian kosong, `tgl_selesai` bernilai `""` dan mobile menampilkan "-" pada ringkasan "Dikembalikan {tgl}".

### RISK-014 — Definisi jatuh tempo berbeda antara chip dan dashboard (RENDAH)
Lihat INC-10: chip tidak mengecualikan peminjaman yang pengembaliannya sedang diajukan dan tidak memasukkan yang sudah lewat; dashboard sebaliknya.

---

## 42. Dependency

```text
Master Company / Data Karyawan / Asset Admin
      ↓
Asset Data (tersedia @gudang)
      ↓
Peminjaman (pengajuan → dikonfirmasi → aktif) ──► Notifikasi
      ↓ Terima Asset → asset_trx_log peminjaman_karyawan
Asset Data (dipinjam @karyawan) ──► Jatuh tempo (Asset Saya, Summary Admin)
      ↓ Pengembalian (header baru)
Asset Data (tersedia / rusak @gudang) ──► asset_trx_log pengembalian_karyawan

Terkait:
  - Onboarding & Request: hasActiveOnboardingAsset dipakai saat pengajuan
  - Offboarding: membuat pengembalian anak (asset_trx_offboarding_relasi); hasActiveOffboardingAsset mencegah pengembalian ganda
  - Asset Saya & Summary Admin: getPendingForEmployee, getPendingForAdmin, getJatuhTempo, getPeminjamanPage (aktif)
  - SQL manual `sql-peminjaman-status-dibatalkan.sql` (enum `dibatalkan`) harus dijalankan sebelum API v1.6
```

---

## 43. Source Code Reference

| File | Layer | Function |
|---|---|---|
| `API/routes/modules/asset/RouteAsset.php` | Route | 15 endpoint |
| `API/app/Http/Controllers/Asset/Admin/AssetAdminPeminjaman_c.php`, `API/app/Http/Controllers/Asset/Admin/AssetAdminPengembalian_c.php` | Controller | Admin peminjaman & pengembalian |
| `API/app/Http/Controllers/Asset/Karyawan/AssetKaryawanPeminjaman_c.php` | Controller | Karyawan + `prosesSerahTerimaPeminjaman` |
| `API/app/Models/Asset/AssetPeminjamanKaryawan_m.php` | Model | Seluruh logika |
| `API/app/Models/Asset/PeminjamanStatus.php` | Enum | Status |
| `API/app/Models/Asset/Asset_m.php` | Model | Helper bersama |
| `API/app/Models/Asset/AssetOffboardingKaryawan_m.php` | Model | Pengembalian via offboarding, `hasActiveOffboardingAsset` |
| `API/app/Models/Asset/AssetKaryawan_m.php`, `AssetAdmin_m.php` | Model | Asset Saya, summary |
| `API/app/Support/Asset/AssetNotification.php` | Support | Event `peminjaman.*`, `pengembalian.*` |
| `API/diagram-asset.dbml`; migrations `2026_09_17_000006`, `2026_09_17_000009` | DB | Struktur, index, enum (`status_peminjaman` + `dibatalkan` bertanda `[NEW KRY05]`) |
| `Claude outputs/sql-peminjaman-status-dibatalkan.sql` | SQL manual | `ALTER … MODIFY COLUMN status` + verifikasi + opsi merapikan data lama + rollback |
| `API/routes/modules/asset/README.md` | Dokumentasi API | Kontrak list/detail/batalkan v1.6 |
| `API/tests/Asset/AssetModelTest.php`, `AssetPeminjamanLocaleTest.php` | Test API | Batal pengajuan/dikonfirmasi, kondisi, riwayat, pengembalian offboarding, validasi locale |
| `MOBILE/test/asset/asset_peminjaman_test.dart`, `asset_peminjaman_admin_test.dart` | Test mobile | 4 tab, kondisi, riwayat, detail pengembalian, 320 px |
| `MOBILE/lib/Repository/Model/PageModel/mAssetPage.dart` | i18n model | Kunci lokalisasi baru |
| `MOBILE/lib/Repository/Service/assetPeminjamanApi.dart`, `assetPeminjamanAdminApi.dart` | Api | Endpoint |
| `MOBILE/lib/Repository/Model/Asset/mAssetPeminjaman.dart`, `mAssetPeminjamanAdmin.dart` | Model | – |
| `MOBILE/lib/Bloc/Asset/AssetPeminjamanBloc.dart`, `AssetPeminjamanAdminBloc.dart` | Bloc | – |
| `MOBILE/lib/UI/Modul/Asset/AssetSaya/Peminjaman/*` | UI karyawan | – |
| `MOBILE/lib/UI/Modul/Asset/AssetAdmin/AssetAdminKaryawan/Peminjaman/*` | UI admin | – |
| `MOBILE/lib/UI/Modul/Asset/AssetSaya/HalamanAssetSaya.dart` | UI | Banner & kartu |
| `MOBILE/lib/Setting/router.dart`, `Bloc/NavigatorBloc.dart` | Navigasi | – |
| `MOBILE/assets/id/asset.json` | i18n | Label |

---

## 44. Developer Notes

- **Satu tabel dua jenis baris**: selalu filter `jenis_status`. Pengembalian adalah header baru, bukan update status peminjaman.
- **Peminjaman asal ditutup otomatis** saat tidak ada lagi asset pinjaman yang dipegang karyawan.
- **Makna `status_item` bergantung jenis baris**; `getDetail` memetakan `disetujui`/`ditolak` → `normal`/`rusak` untuk pengembalian. Pertahankan pemetaan ini bila mengubah enum.
- **Pengembalian offboarding** ditandai lewat `asset_trx_offboarding_relasi` dan selalu dikecualikan dari list/detail/konfirmasi peminjaman (cek `Schema::hasTable` sebelum dipakai).
- **List peminjaman** menyembunyikan `aktif` yang asetnya sudah tidak dipegang karyawan (mis. sudah dikembalikan lewat jalur lain).
- **Jadwal serah terima** divalidasi di model (bukan di request rule) terhadap periode pinjam.
- **`sisa_hari`** dihitung `today → tgl_dikembalikan` (negatif = terlambat); pengingat mulai H-1.
- **Konstanta status** memakai `PeminjamanStatus` enum (konsisten), berbeda dengan modul Request.
- **Status `dibatalkan` (v1.6)**: hanya untuk baris `jenis_status = peminjaman`; deploy SQL enum lebih dulu. Hanya asset `proses` + gudang yang dilepas, tanpa `asset_trx_log`.
- **List peminjaman paginasi**: `limit+1` tanpa COUNT; `kondisi` hanya untuk `tab=aktif`; tab `riwayat` memakai `reorder()` ke `tgl_update` desc dan tidak ada di `filters` statis.
- **Riwayat & detail**: `pengembalian[]` ditempel dari query terpisah (satu query per halaman di list, `getDetail` per pengembalian di detail). Pengembalian offboarding sengaja ikut (keputusan produk).
- **Mobile**: `AssetPeminjamanPageMode` kini empat nilai; pemanggil lama dipetakan di `_assetPeminjamanModeFrom`. State daftar per tab dijaga `_listMatchesMode`.
- **Technical debt**: RISK-001, 003, 004, 010, INC-02/07/10.

---

## 45. End-to-End Traceability

### 45.1 Tanggal kembali → pengingat
```text
Karyawan memilih rentang tanggal di Form Peminjaman
  → _returnDate → AssetPeminjamanRequest.tanggalKembali
  → JSON "tgl_kembali" (after_or_equal:tgl_pinjam)
  → store() → asset_trx_peminjaman_karyawan.tgl_dikembalikan
  → getJatuhTempo(): tgl_dikembalikan ≤ besok → "sisa_hari"
  → Asset Saya: banner jatuh tempo; detail: "terlambat" → label "Terlambat"
```

### 45.2 Asset → dipinjam → kembali
```text
Karyawan memilih asset (Tambah Pinjam Asset) → asset_ids
  → store(): item + asset_data.status_asset = proses
Admin: Setujui → items[].status_item = disetujui → item.status_item
Karyawan: Terima Asset → terimaKaryawan()
  → asset_data: dipinjam, pemegang_asset = karyawan, id_pemegang_asset = id_karyawan
  → asset_trx_log (peminjaman_karyawan)
Karyawan: Kembalikan Asset → header pengembalian (id_peminjaman_asal)
Admin: Normal → items[].kondisi = normal → item.status_item = disetujui
  → asset_data: tersedia, pemegang_asset = gudang, id_asset_lokasi_m = items[].id_asset_lokasi_m
  → asset_trx_log (pengembalian_karyawan) → peminjaman asal: selesai
```

### 45.3 DB → UI
```text
asset_trx_peminjaman_karyawan_item.ket_tolak (baris pengembalian)
  → getDetail() → "assets[].ket_tolak" (status_item = rusak)
  → AssetPeminjamanAsset → "Keterangan Kerusakan" di Detail Pengembalian
```

### 45.4 Walkthrough Lengkap dengan Contoh Data

Bagian ini mengikuti **satu peminjaman dari awal sampai asset kembali**, langkah demi langkah: apa yang dilakukan pengguna di mobile, apa yang dikirim, apa yang diproses API, dan apa yang tersimpan di database. Data bersifat **sintetis**, tetapi format ID, nama kolom, urutan proses, label status, dan pesan diambil dari kode (`AssetPeminjamanKaryawan_m`, `AssetKaryawanPeminjaman_c`, `AssetAdminPeminjaman_c`, `AssetAdminPengembalian_c`, `AssetNotification`).

**Skenario:** Andi mengajukan pinjaman proyektor dan layar untuk presentasi dinas. Admin menyetujui proyektor dan menolak layar. Andi menerima proyektor, lalu mengembalikannya dalam kondisi rusak.

| Langkah | Pelaku | Aksi | Endpoint |
|:---:|---|---|---|
| 0 | Karyawan | Membuka form, memuat opsi | API-007 `GET /asset/karyawan/peminjaman/options` |
| 1 | Karyawan | **Ajukan Peminjaman** (2 asset) | API-009 `POST /asset/karyawan/peminjaman` |
| 2 | Admin | Setujui 1 asset, tolak 1 asset, isi jadwal, **Submit** | API-003 `POST /asset/peminjaman-karyawan/{id}/konfirmasi` |
| 3 | Karyawan | **Terima Asset** | API-011 `POST /asset/karyawan/peminjaman/{id}/terima` |
| 4 | Karyawan | **Kembalikan Asset** | API-013 `POST /asset/karyawan/peminjaman/{id}/pengembalian` |
| 5 | Admin | Periksa kondisi (Rusak), pilih gudang, **Konfirmasi** | API-006 `POST /asset/pengembalian-karyawan/{id}/konfirmasi` |

#### Data awal

Company `CMP001`. Pengajuan 22 Oktober 2026, periode ID `202610`. Belum ada transaksi peminjaman, log, atau notifikasi pada periode ini.

**`data_karyawan`**

| id_karyawan | nik | nama_lengkap | Peran |
|---|---|---|---|
| KRY0003 | EMP-003 | Andi Contoh | Peminjam |
| KRY0099 | ADM-001 | Siti Admin | Admin Asset (aktor) |
| KRY0100 | ADM-002 | Rudi Admin | Admin Asset lain (ikut menerima notifikasi) |

**`asset_data`** (`status_asset = tersedia`, `pemegang_asset = gudang`, `id_pemegang_asset = ''`, `kondisi_awal = normal`, `id_asset_lokasi_m = CMP001202601001` = Gudang Pusat)

| Kode | id_asset_data | nama_asset | kategori | deskripsi |
|---|---|---|---|---|
| A9 | CMP00120260100009 (PRJ-0002) | Proyektor Mini | Proyektor | Kabel HDMI, tas |
| A10 | CMP00120260100010 (LYR-0001) | Layar Portable | Layar | Tripod |

Lokasi gudang untuk pengembalian: `CMP001202610001` = Gudang Servis (`asset_lokasi_m`, tipe `gudang`, company sama).

Penomoran: header dan item `id_company + YYYYMM + 3 digit`, `no_trx = KRY05-YYYYMMNNN`. **Pengajuan pengembalian memakai urutan yang sama** dengan peminjaman (satu tabel, `jenis_status` berbeda). Log dan notifikasi 5 digit.

---

#### Langkah 0 — Karyawan membuka form

**Mobile:** Asset Saya → **Peminjaman** → **+** → `/asset_tambah_peminjaman` → `LoadAssetPeminjamanOptions` → `AssetPeminjamanApi.getOptions()`.

**Dikirim**
```http
GET /asset/karyawan/peminjaman/options
Authorization: Bearer <access_token_andi>
```

**Diproses API:** `jwt.api` + `jwt.ctx`, lalu `AssetPeminjamanKaryawan_m::getOptions`: asset `tersedia` di `gudang` yang tidak ada di peminjaman berjalan (item yang ditolak tidak mengunci), ditambah daftar keperluan tetap.

**Database:** hanya `SELECT`.

**Diterima mobile:** `keperluan[]` (kantor, dinas, pelatihan, lainnya) dan `assets[]` (A9, A10) seperti API-007. Dipakai pada bottom sheet **Tambah Pinjam Asset** dan **Pilih Keperluan**.

#### Langkah 1 — Andi mengajukan peminjaman

**Mobile:** Andi memilih kedua asset, keperluan **Dinas**, tujuan "Presentasi klien di Surabaya", dan rentang tanggal 24–28 Okt 2026, lalu **Ajukan Peminjaman** → `AssetPeminjamanApi.store(AssetPeminjamanRequest)`.

**Dikirim**
```http
POST /asset/karyawan/peminjaman
```
```json
{
  "keperluan": "dinas",
  "tujuan_peminjaman": "Presentasi klien di Surabaya",
  "tgl_pinjam": "2026-10-24",
  "tgl_kembali": "2026-10-28",
  "asset_ids": ["CMP00120260100009", "CMP00120260100010"]
}
```
`id_karyawan` **tidak** dikirim: diambil dari JWT.

**Diproses API** (`AssetKaryawanPeminjaman_c::storePeminjamanSaya` → `AssetPeminjamanKaryawan_m::store`, satu `DB::transaction`)
1. Validasi: `keperluan` dan `tujuan_peminjaman` wajib (maks 255); `tgl_pinjam` format `Y-m-d` dan tidak boleh sebelum hari ini; `tgl_kembali` tidak boleh sebelum `tgl_pinjam`; `asset_ids` minimal 1, unik.
2. `lockCompany("CMP001")`; `findActiveKaryawan("KRY0003")`.
3. `getLockedAssets`: A9 dan A10 harus `tersedia` + `gudang` (`FOR UPDATE`).
4. `hasActiveOnboardingAsset` dan `hasActivePeminjamanAsset` (peminjaman `pengajuan`/`dikonfirmasi`/`aktif` dengan item tidak ditolak): lolos.
5. Generate ID: header urutan 1 → `CMP001202610001`, `no_trx = KRY05-202610001`; item mulai urutan 1.
6. INSERT header (`status = pengajuan`, jadwal kosong), INSERT item (`status_item = NULL`), UPDATE `asset_data` → `proses`.
7. Setelah commit: `AssetNotification::send('peminjaman.diajukan', …)` ke seluruh admin.

**Hasil database**

`asset_trx_peminjaman_karyawan`

| Kolom | Nilai |
|---|---|
| id_asset_trx_peminjaman_karyawan | CMP001202610001 |
| no_trx | KRY05-202610001 |
| tgl_peminjaman / tgl_dikembalikan | 2026-10-24 / 2026-10-28 |
| keperluan / tujuan_peminjaman | dinas / Presentasi klien di Surabaya |
| tgl_serah_terima / jam_serah_terima / keterangan_serah_terima | 0000-00-00 / 00:00:00 / `''` |
| ket_tolak | `''` |
| id_karyawan / nik_karyawan / nama_karyawan | KRY0003 / EMP-003 / Andi Contoh |
| jenis_status | **peminjaman** |
| id_peminjaman_asal | NULL |
| status | **pengajuan** |
| user_approval / tgl_approval | `''` / NULL |
| tgl_input / user_input | 2026-10-22 09:00:00 / EMP-003 - Andi Contoh |
| tgl_update / user_update | NULL / `''` |

`asset_trx_peminjaman_karyawan_item`

| id_asset_trx_peminjaman_item | id_asset_trx_peminjaman | id_asset_data | nama_asset | deskripsi_asset | kategori_asset | status_item | ket_tolak |
|---|---|---|---|---|---|---|---|
| CMP001202610001 | CMP001202610001 | A9 | Proyektor Mini | Kabel HDMI, tas | Proyektor | NULL | `''` |
| CMP001202610002 | CMP001202610001 | A10 | Layar Portable | Tripod | Layar | NULL | `''` |

`asset_data`: A9 dan A10 `status_asset` **tersedia → proses**, `tgl_update` 09:00:00, `user_update` = EMP-003 - Andi Contoh. Peminjam hanya mengubah status; `pemegang_asset` tetap `gudang`.

`notif_data` (2 baris, satu per admin)

| id_notif | id_karyawan | id_ref | rute | ket_notif_id |
|---|---|---|---|---|
| CMP00120261000001 | KRY0099 | CMP001202610001 | asset_admin_detail_peminjaman | Andi Contoh mengajukan peminjaman asset KRY05-202610001. |
| CMP00120261000002 | KRY0100 | CMP001202610001 | asset_admin_detail_peminjaman | Andi Contoh mengajukan peminjaman asset KRY05-202610001. |

`jenis = asset`, `flag_baca = 0`; teks Inggris ada di `ket_notif_en`.

**Diterima mobile** (HTTP 201)
```json
{ "code": "CREATED", "message": "Pengajuan peminjaman asset berhasil dikirim.",
  "data": { "id": "CMP001202610001", "no_trx": "KRY05-202610001", "status_approval": "pengajuan",
            "status_label": "Menunggu Konfirmasi", "tab": "pengajuan" } }
```

#### Langkah 2 — Admin menyetujui satu asset dan menolak satu asset

**Mobile:** notifikasi membuka `/asset_admin_detail_peminjaman` → `LoadAssetPeminjamanAdminDetail(id)` (`GET /asset/peminjaman-karyawan/CMP001202610001`). Admin menekan **Setujui** pada proyektor, **Tolak** pada layar dengan alasan, lalu **Konfirmasi** → sheet **Konfirmasi Peminjaman Asset** (tanggal serah terima dibatasi periode pinjam) → **Submit** → `AssetPeminjamanAdminApi.konfirmasi`.

**Dikirim**
```http
POST /asset/peminjaman-karyawan/CMP001202610001/konfirmasi
```
```json
{
  "items": [
    { "id_item": "CMP001202610001", "status_item": "disetujui" },
    { "id_item": "CMP001202610002", "status_item": "ditolak", "ket_tolak": "Sedang dijadwalkan servis" }
  ],
  "tgl_serah_terima": "2026-10-24",
  "jam_serah_terima": "08:30",
  "keterangan_serah_terima": "Ambil di ruang IT"
}
```

**Diproses API** (`AssetAdminPeminjaman_c::konfirmasiPeminjamanKaryawan` → `konfirmasiAdmin`)
1. Validasi `items` (id unik, `status_item` ∈ disetujui/ditolak, `ket_tolak` wajib bila ditolak); format tanggal `Y-m-d` dan jam `H:i`. Cek admin.
2. `lockCompany`; header dikunci `FOR UPDATE`; harus berstatus `pengajuan` dan **bukan milik offboarding** (tidak ada relasi di `asset_trx_offboarding_relasi`).
3. **Seluruh item** harus diputuskan ("Seluruh asset peminjaman wajib dikonfirmasi.").
4. UPDATE tiap item: `status_item`, `ket_tolak` (kosong untuk disetujui).
5. Karena ada yang disetujui: tanggal dan jam serah terima **wajib**, formatnya harus valid, dan tanggalnya harus **di dalam periode pinjam** (24–28 Okt) — 24 Okt lolos.
6. UPDATE header: `status = dikonfirmasi`, jadwal, `ket_tolak = ''`, `user_approval`, `tgl_approval`, `tgl_update`, `user_update`.
7. Asset **ditolak** (A10) dikembalikan: `tersedia`, `gudang`, `id_pemegang_asset = ''`. Asset **disetujui** (A9) tidak disentuh, tetap `proses`.
8. Notifikasi `peminjaman.disetujui` ke peminjam.

**Hasil database**

| Tabel | Perubahan |
|---|---|
| item `…001` (A9) | status_item NULL → **disetujui**, ket_tolak `''` |
| item `…002` (A10) | status_item NULL → **ditolak**, ket_tolak = Sedang dijadwalkan servis |
| header | status `pengajuan → dikonfirmasi`; tgl_serah_terima 2026-10-24; jam_serah_terima 08:30:00; keterangan_serah_terima = Ambil di ruang IT; user_approval = ADM-001 - Siti Admin; tgl_approval = 2026-10-22 10:00:00; tgl_update/user_update sama |
| asset_data A10 | status_asset `proses → tersedia`; pemegang_asset gudang; id_pemegang_asset `''` |
| asset_data A9 | Tidak berubah (`proses`) |
| notif_data | + `CMP00120261000003`: id_karyawan KRY0003, id_ref `CMP001202610001`, rute `asset_detail_peminjaman`, "Peminjaman asset KRY05-202610001 telah dikonfirmasi admin." |

**Diterima mobile** (HTTP 200)
```json
{ "code": "UPDATED", "message": "Peminjaman asset berhasil dikonfirmasi.",
  "data": { "id": "CMP001202610001", "no_trx": "KRY05-202610001", "status_approval": "dikonfirmasi",
            "status_label": "Dikonfirmasi", "tab": "dikonfirmasi", "jumlah_disetujui": 1, "jumlah_ditolak": 1 } }
```

#### Langkah 3 — Andi menerima asset

**Mobile:** banner di Asset Saya atau notifikasi → `/asset_detail_peminjaman`. Detail (API-010) menampilkan proyektor **Disetujui Admin**, layar **Ditolak Admin** + alasan, jadwal serah terima, dan panel "Siap untuk meminjam Asset?" dengan tombol **Batalkan** dan **Terima Asset**. Andi menekan **Terima Asset** (dialog konfirmasi) → `AssetPeminjamanApi.terima(id)`.

**Dikirim**
```http
POST /asset/karyawan/peminjaman/CMP001202610001/terima
```
Tanpa body.

**Diproses API** (`prosesSerahTerimaPeminjaman` → `AssetPeminjamanKaryawan_m::terimaKaryawan`)
1. `lockCompany`; header milik karyawan (`FOR UPDATE`), status harus `dikonfirmasi` ("Peminjaman asset belum dapat diterima.").
2. Karyawan aktif; item berstatus `disetujui` = hanya `…001`.
3. Asset-nya dikunci dan harus `proses` + `gudang` ("Satu atau beberapa asset belum siap diserahterimakan.").
4. UPDATE `asset_data` A9 → `dipinjam`, `karyawan`, `id_pemegang_asset = KRY0003`, **`id_asset_lokasi_m = NULL`** (asset keluar dari gudang).
5. UPDATE header → `aktif` (`tgl_update` menjadi **tanggal diterima**).
6. INSERT `asset_trx_log` (`jenis_log = peminjaman_karyawan`) per asset.
7. Notifikasi `peminjaman.diterima` ke admin.

**Hasil database**

| Tabel | Perubahan |
|---|---|
| asset_data A9 | status_asset `proses → dipinjam`; pemegang_asset `gudang → karyawan`; id_pemegang_asset `'' → KRY0003`; id_asset_lokasi_m `CMP001202601001 → NULL`; tgl_update 2026-10-24 08:45:00; user_update = EMP-003 - Andi Contoh |
| header | status `dikonfirmasi → aktif`; tgl_update = 2026-10-24 08:45:00; user_update = EMP-003 - Andi Contoh |

`asset_trx_log` (1 baris)

| Kolom | Nilai |
|---|---|
| id_asset_trx_log | CMP00120261000001 |
| id_asset_data | CMP00120260100009 |
| jenis_log | peminjaman_karyawan |
| id_transaksi / nama_tabel_transaksi | CMP001202610001 / asset_trx_peminjaman_karyawan |
| kondisi_awal / kondisi_akhir | normal / normal |
| posisi_awal / posisi_akhir | gudang / karyawan |
| id_pemegang / nama_pemegang | KRY0003 / Andi Contoh |
| user_input | EMP-003 - Andi Contoh |

`notif_data` (2 baris ke admin): `CMP00120261000004` (KRY0099) dan `CMP00120261000005` (KRY0100), rute `asset_admin_detail_peminjaman`, "Andi Contoh telah menerima asset peminjaman KRY05-202610001."

**Diterima mobile** (HTTP 200)
```json
{ "code": "UPDATED", "message": "Asset peminjaman berhasil diterima.",
  "data": { "id": "CMP001202610001", "no_trx": "KRY05-202610001", "status_approval": "aktif",
            "status_label": "Aktif", "tab": "aktif", "jumlah_asset": 1 } }
```
Selama aktif, Asset Saya menampilkan kartu peminjaman aktif. Pada H-1 sampai `tgl_dikembalikan` muncul pengingat jatuh tempo (`sisa_hari`), dan setelah lewat tanggal itu label menjadi **Terlambat**. Pengingat dihitung saat data dibaca; tidak ada scheduler atau push otomatis.

#### Langkah 4 — Andi mengajukan pengembalian

**Mobile:** di detail peminjaman aktif (`dapat_dikembalikan = true`), **Kembalikan Asset** (dialog "Asset aktif pada peminjaman ini akan diajukan untuk dikembalikan.") → `AssetPeminjamanApi.ajukanPengembalian(id)`.

**Dikirim**
```http
POST /asset/karyawan/peminjaman/CMP001202610001/pengembalian
```
Tanpa body: seluruh asset yang disetujui ikut dikembalikan.

**Diproses API** (`ajukanPengembalianKaryawan`)
1. `lockCompany`; peminjaman milik karyawan harus `aktif`.
2. Item `disetujui` = `…001`; semua asset harus masih `dipinjam` oleh karyawan ini.
3. Tidak boleh ada pengembalian `pengajuan`/`dikonfirmasi` untuk asset itu (`hasActivePengembalianAsset`), dan asset tidak boleh sedang di offboarding (`AssetOffboardingKaryawan_m::hasActiveOffboardingAsset`).
4. Generate ID dengan urutan yang **sama** dengan peminjaman: header urutan 2 → `CMP001202610002`, `no_trx = KRY05-202610002`; item urutan 3.
5. INSERT header `jenis_status = pengembalian` dengan `id_peminjaman_asal`, periode/keperluan/tujuan disalin dari peminjaman, `status = pengajuan`; INSERT item (salinan item disetujui, `status_item = NULL`).
6. `asset_data` **tidak diubah** (A9 tetap `dipinjam`).
7. Notifikasi `pengembalian.diajukan` ke admin (memakai ID pengembalian).

**Hasil database**

Header baru `CMP001202610002`

| Kolom | Nilai |
|---|---|
| no_trx | KRY05-202610002 |
| jenis_status / id_peminjaman_asal | **pengembalian** / CMP001202610001 |
| tgl_peminjaman / tgl_dikembalikan / keperluan / tujuan_peminjaman | disalin dari peminjaman asal |
| tgl_serah_terima / jam_serah_terima | 0000-00-00 / 00:00:00 |
| id_karyawan / nik / nama | KRY0003 / EMP-003 / Andi Contoh |
| status | **pengajuan** |
| tgl_input / user_input | 2026-10-28 16:00:00 / EMP-003 - Andi Contoh |

Item baru: `CMP001202610003`, `id_asset_trx_peminjaman = CMP001202610002`, A9, Proyektor Mini, `status_item = NULL`, `id_asset_lokasi_m_tujuan = ''`.

`notif_data`: `CMP00120261000006` (KRY0099) dan `CMP00120261000007` (KRY0100), id_ref `CMP001202610002`, rute `asset_admin_detail_pengembalian`, "Andi Contoh mengajukan pengembalian asset KRY05-202610002."

**Diterima mobile** (HTTP 201)
```json
{ "code": "CREATED", "message": "Pengajuan pengembalian asset berhasil dikirim.",
  "data": { "id": "CMP001202610002", "no_trx": "KRY05-202610002", "jenis_status": "pengembalian",
            "id_peminjaman_asal": "CMP001202610001", "status_approval": "pengajuan",
            "status_label": "Menunggu Konfirmasi", "tab": "pengajuan", "jumlah_asset": 1 } }
```
Tombol **Kembalikan Asset** hilang karena sudah ada pengembalian berjalan.

#### Langkah 5 — Admin memeriksa pengembalian

**Mobile:** Peminjaman (admin) → tab **Pengembalian** → chip Pengajuan → `/asset_admin_detail_pengembalian` → `GET /asset/pengembalian-karyawan/CMP001202610002`. Detail memuat `lokasi_gudang[]` (gudang milik company) untuk dropdown **Lokasi Tujuan Asset**. Admin memilih **Rusak** pada proyektor, mengetik "Lensa tergores", memilih **Gudang Servis**, lalu **Konfirmasi** (langsung dikirim, tanpa sheet).

**Dikirim**
```http
POST /asset/pengembalian-karyawan/CMP001202610002/konfirmasi
```
```json
{
  "items": [
    { "id_item": "CMP001202610003", "kondisi": "rusak",
      "id_asset_lokasi_m": "CMP001202610001", "keterangan_kerusakan": "Lensa tergores" }
  ]
}
```

**Diproses API** (`AssetAdminPengembalian_c` → `konfirmasiPengembalianAdmin`)
1. Validasi: `kondisi` ∈ normal/rusak, `keterangan_kerusakan` wajib bila rusak, `id_asset_lokasi_m` wajib. Cek admin, `lockCompany`.
2. Header pengembalian harus `pengajuan`; peminjaman asal harus ada dan `aktif`.
3. Semua item wajib diisi kondisi dan lokasi (`resolveItemLocations`: lokasi harus bertipe `gudang`, milik company yang sama, tepat satu kali per item).
4. Asset harus masih `dipinjam` oleh karyawan pemilik transaksi.
5. Per item: UPDATE item (`status_item`: normal → `disetujui`, rusak → `ditolak`; `ket_tolak` = keterangan kerusakan; `id_asset_lokasi_m_tujuan`), UPDATE `asset_data`, siapkan baris log.
6. INSERT `asset_trx_log` (`jenis_log = pengembalian_karyawan`).
7. UPDATE header pengembalian → `selesai` (`user_approval`, `tgl_approval`).
8. Bila **tidak ada** asset pinjaman yang masih dipegang karyawan, peminjaman asal ikut → `selesai` (di sini A10 tidak pernah dipegang, jadi selesai).
9. Notifikasi `pengembalian.dikonfirmasi` ke peminjam.

**Hasil database**

| Tabel | Perubahan |
|---|---|
| item `…003` | status_item NULL → **ditolak** (kode untuk "rusak"); ket_tolak = Lensa tergores; id_asset_lokasi_m_tujuan = CMP001202610001 |
| asset_data A9 | status_asset `dipinjam → rusak`; pemegang_asset `karyawan → gudang`; id_pemegang_asset `KRY0003 → ''`; id_asset_lokasi_m `NULL → CMP001202610001`; ket_rusak = Lensa tergores; user_update = ADM-001 - Siti Admin |
| header pengembalian `…002` | status `pengajuan → selesai`; user_approval = ADM-001 - Siti Admin; tgl_approval = 2026-10-28 16:30:00 |
| header peminjaman `…001` | status `aktif → selesai`; tgl_update/user_update diisi admin |

`asset_trx_log` (1 baris baru)

| Kolom | Nilai |
|---|---|
| id_asset_trx_log | CMP00120261000002 |
| id_asset_data | CMP00120260100009 |
| jenis_log | pengembalian_karyawan |
| id_transaksi / nama_tabel_transaksi | CMP001202610002 / asset_trx_peminjaman_karyawan |
| kondisi_awal / kondisi_akhir | normal / **rusak** |
| posisi_awal / posisi_akhir | karyawan / gudang |
| pemegang_asset / id_pemegang / nama_pemegang | gudang / `''` / `''` |
| user_input | ADM-001 - Siti Admin |

`notif_data`: `CMP00120261000008` ke KRY0003, id_ref `CMP001202610002`, rute `asset_detail_pengembalian`, "Pengembalian asset KRY05-202610002 telah dikonfirmasi admin."

**Diterima mobile** (HTTP 200)
```json
{ "code": "UPDATED", "message": "Pengembalian asset berhasil dikonfirmasi.",
  "data": { "id": "CMP001202610002", "no_trx": "KRY05-202610002", "jenis_status": "pengembalian",
            "id_peminjaman_asal": "CMP001202610001", "status_approval": "selesai", "status_label": "Selesai",
            "tab": "selesai", "jumlah_normal": 0, "jumlah_rusak": 1 } }
```
Peminjaman asal berstatus `selesai` sehingga tidak tampil lagi di tab Permintaan Peminjaman maupun Sedang Dipinjam; peminjaman itu kini dapat ditelusuri di tab Riwayat Peminjaman (lihat variasi di bawah).

---

#### Kondisi akhir data

`asset_trx_peminjaman_karyawan`

| id | no_trx | jenis_status | id_peminjaman_asal | status | user_approval |
|---|---|---|---|---|---|
| CMP001202610001 | KRY05-202610001 | peminjaman | NULL | selesai | ADM-001 - Siti Admin |
| CMP001202610002 | KRY05-202610002 | pengembalian | CMP001202610001 | selesai | ADM-001 - Siti Admin |

`asset_trx_peminjaman_karyawan_item`

| id | header | asset | status_item | ket_tolak | id_asset_lokasi_m_tujuan |
|---|---|---|---|---|---|
| CMP001202610001 | …001 | A9 Proyektor | disetujui | `''` | `''` |
| CMP001202610002 | …001 | A10 Layar | ditolak | Sedang dijadwalkan servis | `''` |
| CMP001202610003 | …002 | A9 Proyektor | ditolak (= rusak) | Lensa tergores | CMP001202610001 |

`asset_data`: A9 → `rusak` @gudang (Gudang Servis), `ket_rusak = Lensa tergores`; A10 → `tersedia` @gudang (tidak pernah berpindah). `asset_trx_log`: 2 baris. `notif_data`: 8 baris. A9 yang rusak kini menjadi kandidat modul Perbaikan, Garansi, atau Disposal.

Perhatikan bahwa pada item pengembalian, `status_item` **dipakai ulang** untuk kondisi (`disetujui` = normal, `ditolak` = rusak); API menerjemahkannya menjadi `normal`/`rusak` saat dibaca.

---

#### Variasi

**Semua asset ditolak.** Pada langkah 2, bila kedua item `ditolak`: tanggal dan jam serah terima tidak diminta, header `status = ditolak`, `ket_tolak = "Seluruh asset peminjaman ditolak."`, jadwal tetap `0000-00-00`/`00:00:00`, kedua asset kembali `tersedia`, notifikasi `peminjaman.ditolak` ke peminjam, message "Peminjaman asset berhasil ditolak.".

**Karyawan membatalkan setelah dikonfirmasi.** Pada langkah 3, tombol **Batalkan** → `POST /asset/karyawan/peminjaman/{id}/batalkan` (tanpa body). Syarat status `pengajuan` atau `dikonfirmasi`. Asset yang disetujui (A9) kembali `tersedia`/`gudang`, header `status = dibatalkan` (v1.6; sebelumnya `ditolak`), `ket_tolak = "Dibatalkan oleh karyawan."`, notifikasi `peminjaman.dibatalkan` ke admin, message "Peminjaman asset berhasil dibatalkan.". Tidak ada baris log.

**Karyawan membatalkan saat pengajuan (v1.6).** Andi menyadari salah memilih asset segera setelah langkah 1, sebelum admin memutuskan. Pada detail transaksi yang masih **Menunggu Konfirmasi** hanya ada tombol **Batalkan** (dialog "Peminjaman yang dibatalkan tidak dapat dipulihkan.").
```http
POST /asset/karyawan/peminjaman/CMP001202610001/batalkan
```
Diproses `batalkanKaryawan`: header `pengajuan` diterima; karena status pengajuan, semua item (`status_item` NULL: A9 dan A10) diambil; kedua asset yang masih `proses` + `gudang` diubah menjadi `tersedia`, `gudang`, `id_pemegang_asset = ''`; header menjadi `dibatalkan`, `ket_tolak = "Dibatalkan oleh karyawan."`, `tgl_update`/`user_update` = EMP-003 - Andi Contoh. Tidak ada baris `asset_trx_log`. Notifikasi `peminjaman.dibatalkan` dikirim ke kedua admin ("Andi Contoh membatalkan peminjaman asset KRY05-202610001."). Peminjaman tampil di tab Permintaan Peminjaman → chip Dibatalkan.
```json
{ "code": "UPDATED", "message": "Peminjaman asset berhasil dibatalkan.",
  "data": { "id": "CMP001202610001", "no_trx": "KRY05-202610001", "status_approval": "dibatalkan",
            "status_label": "Dibatalkan", "tab": "dibatalkan" } }
```
Mencoba membatalkan lagi, atau membatalkan peminjaman yang sudah `aktif`, ditolak dengan HTTP 422 "Peminjaman asset sudah tidak dapat dibatalkan.". Bila admin lebih dulu mengonfirmasi, Andi tetap bisa membatalkan (status `dikonfirmasi`); bila Andi lebih dulu membatalkan, konfirmasi admin ditolak 422 "Peminjaman asset sudah tidak menunggu konfirmasi.".

**Tab Riwayat dengan pengembalian (v1.6).** Setelah langkah 5, peminjaman `CMP001202610001` berstatus `selesai` dan muncul di tab Riwayat Peminjaman sebagai satu entri bersama pengembaliannya.
```http
GET /asset/karyawan/peminjaman?tab=riwayat&limit=20&offset=0
```
```json
{ "code": "SUCCESS", "message": "Data peminjaman asset berhasil ditemukan.",
  "data": {
    "filters": [
      {"value": "pengajuan", "label": "Pengajuan"},
      {"value": "dikonfirmasi", "label": "Dikonfirmasi"},
      {"value": "ditolak", "label": "Ditolak"},
      {"value": "aktif", "label": "Aktif"},
      {"value": "dibatalkan", "label": "Dibatalkan"}
    ],
    "records": [
      { "id": "CMP001202610001", "no_trx": "KRY05-202610001", "tgl_pengajuan": "2026-10-22",
        "keperluan": "dinas", "tujuan_peminjaman": "Presentasi klien di Surabaya",
        "tgl_pinjam": "2026-10-24", "tgl_kembali": "2026-10-28",
        "jenis_status": "peminjaman", "status_approval": "selesai", "status_label": "Selesai", "tab": "selesai",
        "jumlah_asset": 2, "tgl_diterima": "", "terlambat": false,
        "pengembalian": [
          { "id": "CMP001202610002", "no_trx": "KRY05-202610002", "tgl_selesai": "2026-10-28",
            "jumlah_normal": 0, "jumlah_rusak": 1 }
        ] }
    ],
    "pagination": { "limit": 20, "offset": 0, "next_offset": 1, "has_more": false } } }
```
Daftar diurut `tgl_update` desc. `jumlah_asset = 2` karena header menghitung semua item (A9 disetujui dan A10 ditolak). Mobile menampilkan kartu: nomor `KRY05-202610001`, periode 24–28 Okt 2026, "2 Asset", badge **Selesai**, dan ringkasan "Dikembalikan 28 Okt 2026 · 0 normal · 1 rusak". Membuka kartu memanggil detail (API-010) yang memuat `pengembalian[]` lengkap (status Selesai, aset A9 `kondisi = rusak`, `keterangan_kerusakan = Lensa tergores`, `lokasi_tujuan = Gudang Servis`). Admin melihat entri yang sama lewat `GET /asset/peminjaman-karyawan?tab=riwayat` (semua karyawan) dan detailnya read-only.

**Pengembalian normal.** Pada langkah 5, bila `kondisi = normal`: item `disetujui`, A9 → `tersedia` @gudang pilihan admin, `ket_rusak = ''`, log `kondisi_akhir = normal`, `jumlah_normal = 1`.

**Terlambat.** Peminjaman `aktif` setelah `tgl_dikembalikan` tetap `aktif` di database; API menghitung `terlambat = true` dan mobile menampilkan label **Terlambat** serta peringatan pengembalian.

---

#### Kasus gagal yang umum

| Situasi | Langkah | HTTP | Pesan | Perubahan database |
|---|---|:---:|---|---|
| Tanggal pinjam sebelum hari ini | 1 | 422 | Tanggal pinjam tidak boleh sebelum hari ini. | Tidak ada |
| Tanggal kembali sebelum tanggal pinjam | 1 | 422 | Tanggal kembali tidak boleh sebelum tanggal pinjam. | Tidak ada |
| Asset bukan `tersedia` + `gudang` | 1 | 422 | Satu atau beberapa asset tidak tersedia untuk dipinjam. | Tidak ada |
| Asset ada di onboarding aktif | 1 | 422 | Satu atau beberapa asset sedang diproses pada onboarding. | Tidak ada |
| Asset ada di peminjaman lain | 1 | 422 | Satu atau beberapa asset sedang diproses pada peminjaman lain. | Tidak ada |
| Ada item belum diputuskan | 2 | 422 | Seluruh asset peminjaman wajib dikonfirmasi. | Tidak ada |
| Tolak tanpa alasan | 2 | 422 | Alasan penolakan asset wajib diisi. | Tidak ada |
| Ada yang disetujui tanpa jadwal | 2 | 422 | Tanggal serah terima wajib diisi. / Jam serah terima wajib diisi. | Tidak ada |
| Tanggal serah terima di luar periode pinjam | 2 | 422 | Tanggal serah terima harus berada dalam periode peminjaman. | Tidak ada |
| Konfirmasi dua kali | 2 | 422 | Peminjaman asset sudah tidak menunggu konfirmasi. | Tidak ada |
| Pengembalian milik offboarding dikonfirmasi lewat menu ini | 5 | 422 | Pengembalian ini diproses melalui offboarding. | Tidak ada |
| Terima sebelum dikonfirmasi admin | 3 | 422 | Peminjaman asset belum dapat diterima. | Tidak ada |
| Batalkan setelah aktif / selesai / ditolak / dibatalkan | 3 | 422 | Peminjaman asset sudah tidak dapat dibatalkan. | Tidak ada |
| Kembalikan peminjaman yang tidak aktif | 4 | 422 | Hanya peminjaman aktif yang dapat dikembalikan. | Tidak ada |
| Sudah ada pengembalian berjalan | 4 | 422 | Pengembalian asset sedang menunggu konfirmasi. | Tidak ada |
| Asset sedang di offboarding | 4 | 422 | Asset sedang diproses pada offboarding. | Tidak ada |
| Kondisi atau lokasi gudang tidak lengkap | 5 | 422 | Seluruh kondisi asset pengembalian wajib diisi. / Lokasi gudang tujuan setiap asset wajib dipilih. | Tidak ada |
| Lokasi bukan gudang atau company lain | 5 | 422 | Lokasi gudang tujuan tidak valid. / …tidak sesuai. | Tidak ada |
| Rusak tanpa keterangan | 5 | 422 | Keterangan kerusakan asset wajib diisi. | Tidak ada |
| Pengguna bukan admin asset | 2, 5 | 403 | Pengguna tidak memiliki akses admin asset. | Tidak ada |
| Transaksi milik orang lain | 3, 4 | 422 / 404 | Peminjaman asset tidak ditemukan. | Tidak ada |
| Token kedaluwarsa | semua | 401 | Token telah expired. Mobile me-refresh token sekali lalu mengulang request. | Tidak ada |

Semua pelanggaran aturan dilempar sebagai `InvalidArgumentException` di dalam `DB::transaction`, sehingga seluruh perubahan request itu dibatalkan. Pesan tersedia dalam bahasa Indonesia dan Inggris mengikuti header `locale`.

#### Ringkasan siapa mengerjakan apa

| Tahap | Mobile | API | Database |
|---|---|---|---|
| Ajukan | Pilih asset, keperluan, tujuan, rentang tanggal; `AssetPeminjamanRequest` | Validasi, lock, cek asset, ID, insert, reservasi | header, item, `asset_data → proses`, `notif_data` |
| Konfirmasi admin | Setujui/tolak per asset, sheet jadwal | Validasi keputusan dan jadwal dalam periode, lepas asset ditolak | item, header, `asset_data` (ditolak → tersedia), `notif_data` |
| Terima / batalkan | Dialog konfirmasi | Cek status (batal: pengajuan atau dikonfirmasi) dan asset, serah terima atau pelepasan | header (aktif / dibatalkan), `asset_data`, `asset_trx_log` (terima saja), `notif_data` |
| Riwayat | Tab Riwayat Peminjaman, kartu ringkasan pengembalian | `tab=riwayat` + `attachRiwayatPengembalian` / `pengembalian[]` di detail | `SELECT` saja |
| Ajukan pengembalian | Tombol Kembalikan Asset | Cek asset masih dipinjam dan tidak di offboarding, salin item | header + item baru, `notif_data` |
| Konfirmasi pengembalian | Kondisi + gudang per asset | Validasi lokasi, update asset, tutup peminjaman asal | item, `asset_data`, `asset_trx_log`, header (2), `notif_data` |
| Tampilan | JSON → model → widget; stepper dan timeline | `peminjamanStatusMetadata`, `terlambat`, `dapat_dikembalikan` | `SELECT` saja |

---

## 46. Ringkasan Database Impact

| Event | INSERT | UPDATE | DELETE |
|---|---|---|---|
| Ajukan peminjaman | header, item, notif | asset_data → proses | – |
| Konfirmasi admin | notif | item (keputusan), header (dikonfirmasi/ditolak, jadwal), asset_data (ditolak → tersedia) | – |
| Terima Asset | asset_trx_log, notif | asset_data → dipinjam @karyawan, header → aktif | – |
| Batalkan (pengajuan atau dikonfirmasi) | notif | asset_data → tersedia (pengajuan: semua item; dikonfirmasi: item disetujui; hanya `proses` + gudang), header → dibatalkan | – |
| Ajukan pengembalian | header pengembalian, item, notif | – | – |
| Konfirmasi pengembalian | asset_trx_log, notif | item (kondisi, lokasi tujuan), asset_data → tersedia/rusak @gudang + lokasi, header pengembalian → selesai, peminjaman asal → selesai | – |

---

## 47. Checklist Implementasi

### Database
- [x] Header (peminjaman & pengembalian) & item
- [x] Relasi `id_peminjaman_asal`
- [x] Index list admin & karyawan
- [x] Enum status
- [x] Nilai enum `dibatalkan` di DBML dan SQL manual `sql-peminjaman-status-dibatalkan.sql` (eksekusi di server: ⚠️ belum diverifikasi, prasyarat deploy)
- [ ] Rapikan data lama "Dibatalkan oleh karyawan." dari `ditolak` ke `dibatalkan` (SQL langkah 3, opsional)
- [x] Log serah terima & pengembalian
- [ ] Migration CREATE TABLE baseline
- [ ] Kolom tanggal diterima & kondisi terpisah

### Backend
- [x] Ajukan, konfirmasi per asset + jadwal, terima, batalkan
- [x] Ajukan & konfirmasi pengembalian
- [x] Jatuh tempo & terlambat (tampilan)
- [x] Integrasi offboarding
- [x] Batal pengajuan oleh karyawan (v1.6)
- [x] Filter `kondisi` dan tab `dibatalkan`/`riwayat`; `pengembalian[]` di list riwayat dan detail
- [ ] Batal/kedaluwarsa oleh admin
- [ ] Tolak pengembalian
- [ ] Scheduler pengingat jatuh tempo
- [ ] Perpanjangan & pengembalian sebagian

### Mobile
- [x] List peminjaman/pengembalian (karyawan & admin)
- [x] Form pengajuan dengan picker asset & rentang tanggal
- [x] Detail + Terima/Batalkan/Kembalikan
- [x] Konfirmasi admin + sheet jadwal
- [x] Konfirmasi kondisi pengembalian
- [x] Lokasi gudang tujuan per asset saat pengembalian; lokasi dikosongkan saat asset keluar gudang (v1.1)
- [ ] Backfill lokasi asset yang sudah di gudang
- [x] Banner & kartu di Asset Saya
- [x] Halaman 4 tab (Permintaan, Sedang Dipinjam, Pengembalian, Riwayat) dengan chip kondisi dan kartu Riwayat (v1.6)
- [x] Batalkan pada detail status pengajuan; admin read-only pada peminjaman selesai
- [ ] `maxLength` tujuan peminjaman

### Documentation
- [x] Business Flow, DB, API, Mobile, Mapping, Manual Book, Testing
- [x] Disinkronkan dengan kode 2026-10-27 (v1.6)
- [ ] Screenshot

---

## 48. Changelog

| Version | Date | Changes |
|---|---|---|
| 1.6 | 2026-10-27 | (A) Status baru **`dibatalkan`** (enum `status_peminjaman`; SQL manual `sql-peminjaman-status-dibatalkan.sql`, prasyarat deploy): karyawan dapat membatalkan saat `pengajuan` dan `dikonfirmasi`, hasil `dibatalkan` (bukan `ditolak`), semua/asset disetujui dilepas, tanpa `asset_trx_log`; BR-012, §14, §15, §21.4, API-012, notifikasi; RISK-002 ditutup, INC-03 diperbaiki. (B) Halaman **4 tab** (Permintaan Peminjaman, Sedang Dipinjam, Pengembalian, Riwayat Peminjaman) untuk karyawan dan admin; `AssetPeminjamanPageMode` = {peminjaman, sedangDipinjam, pengembalian, riwayat}; pemetaan pemanggil lama; §25–§30, §37. (C) API list: parameter `kondisi` (`semua|jatuh_tempo|terlambat`, hanya `tab=aktif`, difilter sebelum paginasi), BR-019, INC-10. (D) Tab **Riwayat** = peminjaman `selesai` + `pengembalian[]` (list) dan `pengembalian[]` pada detail; pengembalian offboarding sengaja ikut tampil; kartu Riwayat, bagian Pengembalian, admin read-only pada selesai (BR-017, BR-020, INC-04 diperbaiki). (E) §39/§41: RISK-010–014, INC-10/11. (F) Tes: API `tests/Asset` 386 lulus (4850 assertion, sqlite); mobile `test/asset` 466 lulus + 5 gagal lama tidak terkait (hasil eksekusi sesi 2026-10-27, tidak dijalankan ulang saat dokumen ini disusun); TC-41–TC-63; §45.4 menambah skenario batal saat pengajuan dan tab Riwayat. Jumlah rute tetap 15. |
| 1.5 | 2026-10-22 | Menambah §45.4 *Walkthrough Lengkap dengan Contoh Data* (ajukan, konfirmasi admin per asset, terima, ajukan pengembalian, konfirmasi pengembalian dengan lokasi gudang; variasi tolak semua, batalkan, pengembalian normal, terlambat; kasus gagal). Sinkronisasi kode: nama tabel log yang benar adalah `asset_trx_log` (`id_asset_trx_log`). |
| 1.4 | 2026-10-20 | Detail peminjaman (karyawan & admin): kartu Data Karyawan kini menampilkan Jabatan, Cabang, Departemen (label Divisi diganti Departemen). API: `karyawan.jabatan` ditambahkan di `getDetail` (leftJoin `master_jabatan`, tanpa SQL/migrasi); mobile: field `jabatan` di `AssetPeminjamanKaryawan`; tes `asset_karyawan_jabatan_test.dart`. |
| 1.3 | 2026-10-12 | Mobile: stepper alur status (`AssetAlurStepper.peminjaman`) dan kartu timeline vertikal per aset (`AssetPeminjamanTimelineCards`) di detail peminjaman/pengembalian karyawan dan admin (§26.3, 26.5, 26.6). Teks langkah di grup JSON `alur`. Tidak ada perubahan API. |
| 1.2 | 2026-10-12 | Sinkronisasi dengan kode 12 Okt 2026: path file mobile mengikuti struktur folder baru (`UI/Modul/Asset/<Fitur>/` berhuruf kapital, model di `Repository/Model/Asset/`). |
| 1.1 | 2026-10-05 | Konfirmasi pengembalian kini wajib memilih **lokasi gudang per asset** (`items[].id_asset_lokasi_m`; kolom `asset_trx_peminjaman_karyawan_item.id_asset_lokasi_m_tujuan` dan `asset_data.id_asset_lokasi_m`); lokasi dikosongkan saat karyawan menerima pinjaman. BR-014/015/015a, TC-38..40, mobile dropdown Lokasi Tujuan Asset. Nama controller disesuaikan (`AssetAdminPeminjaman_c`, `AssetAdminPengembalian_c`, `AssetKaryawanPeminjaman_c`) |
| 1.0 | 2026-09-26 | Initial documentation — disusun dari source lokal API (Lumen) & MOBILE (Flutter) |
