# Admin rebuild (CI4 + TailAdmin + API)

Ini **bukan** migrasi modul admin HMVC CodeIgniter 3. Panel admin dibangun ulang di CodeIgniter 4 dengan UI Tailwind (aset TailAdmin di `public/assets/tailadmin/`) dan **tanpa query database langsung dari controller web admin**. Semua data modul Pegawai, Presensi, Laporan ringkasan, Dashboard beranda, dan Cuti diambil lewat **API internal** (`/api/admin/*` dan untuk profil/berita tetap `/api/mobile/*`).

## Prinsip

- Controller di `app/Controllers/Admin/` hanya memanggil `ApiClient` → HTTP ke aplikasi yang sama.
- Tidak menyalin HMVC CI3, tidak memakai Grocery CRUD.
- Akses database untuk fitur admin terpusat di `app/Services/Admin/AdminApiService.php` + model CI4 (`PegawaiModel`, dll.).
- Respons API diseragamkan: `{ "status": 0|1, "pesan": "...", "data": ... }` (selaras API mobile).

## RBAC (peran & fitur)

- **Konfigurasi:** `app/Config/AdminAccess.php` memetakan **nama fitur** → daftar **slug grup Ion** yang boleh. Daftar kosong artinya **semua admin yang sudah login** (setelah `authadmin`).
- **Helper:** `app/Helpers/rbac_helper.php` — `hasRole()`, `hasAnyRole()`, `canAccess($feature)`. Membaca **`admin_ion_groups`** dari sesi (nama grup, perbandingan case-insensitive).
- **Kapan aturan Ion dipakai:** jika `admin_auth_source === 'admin_users'`. Login **pegawai** saja (tanpa grup Ion di sesi) mempertahankan perilaku lama: **`canAccess` mengizinkan semua fitur** yang punya daftar grup (kompatibel token proxy). Ini selaras kebutuhan token API pegawai untuk semua endpoint admin.
- **Jika `admin_users` tetapi grup kosong:** fitur yang membutuhkan grup tertentu → **ditolak** (menu disembunyikan, `enforceAccess` redirect dengan flash).
- **Controller web:** `BaseAdminController::enforceAccess('nama_fitur')` di awal aksi; gagal → redirect ke `/admin` dengan flash **error**, atau JSON 403 untuk AJAX.
- **View:** hanya memanggil `canAccess('...')` — **tanpa menuliskan nama peran** di view; semua mapping ada di `AdminAccess`.

### Pemetaan fitur ↔ grup (sama `ci_bootstrap.php` CI3)

| Fitur (`canAccess`) | Grup yang boleh |
|---------------------|-----------------|
| `dashboard` | (semua login admin) |
| `presensi` | (semua) |
| `perusahaan` | (semua) |
| `pegawai` | `webmaster`, `hrd` |
| `cuti` | `webmaster`, `hrd` |
| `laporan` | `webmaster`, `hrd` |
| `panel` | `webmaster` |
| `utilitas` | `webmaster` |
| `apk_link` | `webmaster`, `penyelenggara`, `operator_cabang`, `operator_ranting`, `operator_sekolah`, `admin_soal`, `operator_soal` |
| `references` | (semua login admin) — referensi form pegawai |

## Autentikasi web

1. `GET/POST /admin/login` — `Auth::attempt` urutannya:  
   - **Pegawai:** `POST /api/mobile/login` (MD5 password, tabel `pegawai`).  
   - Jika gagal: **`admin_users`** (Ion Auth CI3): `password_verify` terhadap bcrypt/hash di kolom `password`, `active = 1`. Jika sukses, sistem menerbitkan token API dengan memakai **pegawai proxy** (lihat bawah) dan menyimpan token yang sama di sesi `admin_mobile_token`. Sesi juga menyimpan `admin_auth_source` = `pegawai` | `admin_users`.
2. **Data Ion Auth di DB:** `admin_users`, `admin_groups`, `admin_users_groups`. Login `admin_users` **wajib punya minimal satu baris** di `admin_users_groups` bila tabel itu ada — tanpa grup, login ditolak.
3. **Proxy token (login `admin_users`):** API hanya mengenali token di `pegawai`. Setelah Ion Auth sukses, token ditulis ke pegawai proxy: `ADMIN_LOGIN_PROXY_PEGAWAI_ID` (`.env`), lalu pegawai pertama `super_akses = 'true'`, lalu `id_pegawai` terkecil. Untuk akun **hanya HRD**, bisa set `ADMIN_LOGIN_PROXY_PEGAWAI_ID_HRD`.
4. Sesi menyimpan `admin_ion_user_id` dan **`admin_ion_groups`** (nama grup) bila sumber `admin_users` — dipakai RBAC + header.
5. `GET /admin/logout` — di luar filter.
6. Rute `admin/*` lain memakai filter **`authadmin`**: tanpa token sesi → redirect ke login (atau JSON 401 untuk AJAX).

## API admin (`/api/admin/*`)

Parameter **`token`** (sama dengan `admin_mobile_token` di sesi panel) tetap dikirim lewat query GET, field POST, atau header `X-Admin-Token` — **struktur token untuk klien mobile tidak diubah** (`/api/mobile/*` tidak tersentuh).

### Admin API Security

- **Sesi panel wajib:** setiap `/api/admin/*` memverifikasi bahwa permintaan membawa **cookie sesi** yang valid dan berisi `admin_mobile_token`, serta bahwa **`token` di URL/body/header sama persis** dengan nilai di sesi. Tanpa itu, respons **`401`** dengan JSON `{ "status": 0, "pesan": "Unauthorized" }` — sehingga **curl/Postman hanya dengan `?token=` pegawai** (tanpa cookie login admin) ditolak.
- **RBAC di API:** setelah sesi valid, `BaseAdminApiController::requireAdminApiAccess('fitur')` memanggil `canAccess()` (sama `Config\AdminAccess` + `admin_ion_groups` seperti UI). Gagal → **`403`** dengan `{ "status": 0, "pesan": "Forbidden" }`.
- **Penerusan cookie internal:** `ApiClient::getAdmin` / `postAdmin` meneruskan header `Cookie` dari permintaan browser ke hit HTTP internal, dan memanggil `session_write_close()` sebelum CURL agar menghindari **deadlock** file session (sub-request dapat membuka sesi yang sama).
- **Logging teks:** percobaan ditolak dicatat ke log aplikasi (`log_message('warning', ...)`) dengan alasan singkat, alamat IP, path URI, dan timestamp ISO-8601; lihat juga **Audit Logging System** di bawah untuk rekaman DB.
- **Token admin terpisah:** tidak wajib; tidak mengganti model sesi saat ini.

### Audit Logging System

- **Tabel:** `admin_activity_logs` (migrasi `app/Database/Migrations/2026-04-18-140000_CreateAdminActivityLogs.php`). Kolom: `id`, `admin_user`, `action`, `endpoint`, `payload` (JSON teks), `ip_address`, `user_agent`, `auth_source`, `roles_json` (JSON grup Ion dari sesi), `outcome` (`success` | `unauthorized` | `forbidden`), `created_at`.
- **Layanan:** `app/Services/AdminAuditService.php` — metode **`log(string $action, array $payload = [])`**. Menyisipkan konteks HTTP (`_http`), panel (`_session_panel`: sumber auth, grup Ion, indikator token admin), ringkasan **actor pegawai** (dari token API), dan payload bisnis. Nilai sensitif (`password`, `token`, dll.) diganti `[redacted]`.
- **Integrasi API:** setelah `requireAdminApiAccess` sukses, setiap endpoint di `app/Controllers/Api/Admin/*` memanggil `auditAuthorized()` di `BaseAdminApiController` dengan nama aksi konsisten (`api.admin.cuti.approve`, `api.admin.pegawai.update`, …). Mutasi penting (approve/reject cuti, create/update/delete/reset pegawai) menyertakan ID / field ringkas di payload.
- **Keamanan:** percobaan **Unauthorized** / **Forbidden** juga ditulis lewat `log()` dengan `__outcome` = `unauthorized` | `forbidden` dan aksi `api.admin.security.unauthorized` / `api.admin.security.forbidden`.
- **Performa:** insert sinkron ringan; payload dibatasi panjang; jika tabel belum ada, tulisan dilewati (debug log) agar tidak memutus deploy.

| Metode | Path | Keterangan |
|--------|------|------------|
| GET | `/api/admin/references` | Dropdown: jabatan, unit_kerja, golongan, kantor, jadwal |
| GET | `/api/admin/dashboard` | Statistik beranda + antrian cuti Waiting (20 baris + total) |
| GET | `/api/admin/pegawai` | Daftar pegawai + pagination (`page`, `per_page`, `q`) |
| GET | `/api/admin/pegawai/{id}` | Detail pegawai |
| POST | `/api/admin/pegawai/create` | Tambah pegawai |
| POST | `/api/admin/pegawai/update` | Body termasuk `id_pegawai` |
| POST | `/api/admin/pegawai/delete` | Body `id_pegawai` |
| POST | `/api/admin/pegawai/reset_password` | Body `id_pegawai` |
| GET | `/api/admin/presensi` | Daftar presensi (`tanggal_dari`, `tanggal_sampai`, `page`, `per_page`, **`q`** nama/NIP) |
| GET | `/api/admin/presensi/{id}` | Detail presensi |
| GET | `/api/admin/laporan` | Ringkasan agregat (`dari`, `sampai`) |
| GET | `/api/admin/cuti` | Daftar cuti (`status` kosong=semua, `Waiting`, `Approve`, `Rejected`, `Cancelled`, `page`, `per_page`) |
| GET | `/api/admin/cuti/{id}` | Detail cuti + daftar file dokumen |
| POST | `/api/admin/cuti/approve` | Body `id_cuti` |
| POST | `/api/admin/cuti/reject` | Body `id_cuti`, `alasan_tolak` |
| GET | `/api/admin/laporan/cuti` | Laporan cuti rentang (`dari`, `sampai`) |
| GET/POST | `/api/admin/company/*` | Master perusahaan (kantor, unit, golongan, jabatan, berita) |
| GET/POST | `/api/admin/presensi/dilapangan|lembur|libur|jadwal|aktivitas` | Alat presensi CI3 |
| GET/POST | `/api/admin/panel/users|groups|…` | Panel pengguna Ion |
| GET/POST | `/api/admin/backup*` | Daftar / jalankan / hapus file backup SQL |

Controller API: `app/Controllers/Api/Admin/*`. Rute di `app/Config/Routes.php` grup `api/admin`. Dasar kelas: `BaseAdminApiController` (sesi + ikatan token + RBAC per fitur).

## Rute web admin

| Path | Fungsi |
|------|--------|
| `/admin/login`, POST login | Login |
| `/admin/logout` | Keluar |
| `/admin` | Dashboard (API `dashboard` + profil/berita mobile) |
| `/admin/pegawai` | Daftar pegawai |
| `/admin/pegawai/create`, POST `store` | Tambah |
| `/admin/pegawai/edit/{id}`, POST `update/{id}` | Edit |
| POST `pegawai/delete/{id}` | Hapus |
| POST `pegawai/reset/{id}` | Reset password |
| `/admin/presensi` | Daftar + filter tanggal + **cari nama/NIP** |
| `/admin/presensi/detail/{id}` | Detail |
| `/admin/presensi/lapangan` … `aktivitas` | Master alat presensi (dilapangan, lembur, libur, jadwal, aktivitas) |
| `/admin/perusahaan/kantor` … `berita` | CRUD master perusahaan |
| `/admin/laporan` | Ringkasan + filter tanggal |
| `/admin/laporan/cuti` | Tabel cuti rentang tanggal |
| `/admin/panel/users`, `create`, `reset/{id}` | Pengguna admin Ion |
| `/admin/util/backup` | Backup DB (file di `writable/admin_db_backup/`) |
| `/admin/cuti` | Daftar cuti + filter status |
| `/admin/cuti/detail/{id}` | Detail + approve/reject |
| POST `/admin/cuti/approve/{id}` | Setujui |
| POST `/admin/cuti/reject/{id}` | Tolak + alasan |

## `ApiClient` & `BaseAdminController`

- `getAdmin` / `postAdmin` — ke `/api/admin/*` dengan token otomatis + **penerusan `Cookie`** untuk sesi panel.
- `BaseAdminController::enforceAccess($feature)` — RBAC fitur di UI web.

## Konfigurasi

`app.baseURL` di `.env` harus benar agar panggilan internal CURLRequest berhasil.

## Modul vs CI3 — parity & celah

**Sudah mendekati CI3 (usable):**

- Dashboard beranda: kartu pegawai/presensi/cuti hari ini, bar ringkasan, tabel permohonan cuti Waiting + aksi cepat (bila `canAccess('cuti')`).
- Cuti admin: daftar, detail, approve/reject (status `Approve` / `Rejected` + `alasan_tolak`), dokumen di `assets/uploads/dokcuti/`.
- Pegawai, presensi (dengan badge status baris), laporan ringkasan.

**Masih berbeda / lanjutan:**

- **Lingkup data** di `AdminApiService` tetap mengikuti baris pegawai yang memiliki `token` (proxy); **siapa boleh memanggil endpoint** kini dijaga sesi + RBAC seperti UI.
- Laporan **keuangan** CI3 (piutang, penjualan, laba, dll.) tidak di-port ke modul admin CI4 ini.
- **Cetak** khusus seperti PHPExcel di CI3 diganti dengan tabel web + cetak browser / integrasi via endpoint JSON bila diperlukan.

## Peta halaman CI3 (discovery)

Daftar lengkap URL, controller, method, peran, dan pemetaan ke CI4 ada di **`docs/migration/admin-pages-map.md`**. File itu menjadi acuan agar tidak ada halaman CI3 yang “hilang” dari checklist tanpa sengaja.

## UI shell TailAdmin (dashboard)

**Tujuan:** tampilan konsisten seperti demo TailAdmin (sidebar terang, border `gray-200`, kartu `rounded-2xl`, header sticky, tabel dengan header abu), **tanpa** memuat `public/assets/tailadmin/css/style.css` mentah di browser — file itu memakai `@import "tailwindcss"` / `@apply` dan membutuhkan **build npm**, bukan CSS siap pakai. Untuk kelas utilitas dari markup PHP, tetap memakai **Tailwind CDN** di `layouts/main.php` + **`layouts/auth.php`**; aset **lokal** yang dipakai: Font Awesome (`fa-7.1.0-web/css/all.min.css`) dan logo brand `public/assets/images/bpr-logo.png` (sidebar + halaman login).

**Layout admin (`layouts/main.php`):**

- `#admin-shell`: flex penuh tinggi viewport, latar `bg-gray-50`.
- **Sidebar** (`layouts/sidebar.php`): lebar 290px, putih, border kanan; menu mengikuti `ci_bootstrap.php` CI3; teks judul memakai kelas `sidebar-text` / `nav-section-label` agar bisa disembunyikan saat mode **sempit desktop** (`sidebar-narrow` + localStorage).
- **Mobile (&lt; lg):** sidebar `transform: translateX(-100%)`; tombol hamburger di header membuka kelas `mobile-sidebar-open` pada `#admin-shell`; overlay `#sidebar-overlay` menutup panel ketika diketuk.
- **Desktop:** tombol kolom di header men-toggle `sidebar-narrow` (lebar sidebar 90px, label disembunyikan).
- **Header** (`layouts/header.php`): sticky, flash message, info sesi singkat.
- **Footer** (`layouts/footer.php`): strip tipis di bawah konten scroll.

**Struktur view modul (rapi):**

| Modul | Path view |
|-------|------------|
| Beranda | `app/Views/admin/dashboard/index.php` |
| Login | `app/Views/admin/auth/login.php` |
| Pegawai | `app/Views/admin/pegawai/index.php`, `form.php` |
| Presensi | `app/Views/admin/presensi/index.php`, `detail.php` |
| Cuti | `app/Views/admin/cuti/index.php`, `detail.php` |
| Laporan | `app/Views/admin/laporan/index.php`, `laporan/cuti.php` |
| Perusahaan | `app/Views/admin/perusahaan/*.php` |
| Presensi (alat) | `app/Views/admin/presensi/lapangan.php`, `lembur.php`, `libur.php`, `jadwal.php`, `aktivitas.php` |
| Panel | `app/Views/admin/panel/users.php`, `user_create.php`, `user_reset.php` |
| Utilitas | `app/Views/admin/util/backup.php` |

Semua halaman admin di atas memakai `<?= $this->extend('layouts/main') ?>` (login memakai `layouts/auth`).

## Halaman selesai vs sisa (web admin)

**Selesai (parity operasional harian + UI TailAdmin):**

- Beranda / dashboard (`/admin`)
- Data presensi + detail + pencarian (`/admin/presensi`, detail)
- Presensi: tugas luar/lapangan, lembur, management jadwal, hari libur, rekaman aktivitas
- Perusahaan: lokasi kerja (kantor), unit kerja, jabatan, golongan, berita/pengumuman
- Data pegawai + form tambah/edit + hapus + reset password (`/admin/pegawai`, …)
- Data cuti + detail approve/reject (`/admin/cuti`, detail)
- Ringkasan laporan (`/admin/laporan`) + laporan cuti rentang (`/admin/laporan/cuti`, cetak via browser)
- Panel: daftar pengguna admin Ion, tambah pengguna (bukan webmaster), reset password
- Utilitas: daftar backup SQL, jalankan backup, unduh, hapus file (`writable/admin_db_backup/`)
- Login (`/admin/login`)

**Belum di-port dari CI3 (sengaja / luar cakupan modul ini):**

- Laporan modul **keuangan** (jatuhtempo, penjualan, laba, cetak terkait)
- Panel: kelola grup (`admin_user_group`), halaman profil/ubah password akun admin terpisah (bisa memakai alur pegawai/API)
- Utilitas: **restore** database dari file (hanya backup + unduh + hapus)
- Unggah foto berita lewat form file (CI4 saat ini: isi nama file manual jika file sudah di server)

## Catatan parity UX

- Urutan dan label menu utama mengikuti **sidemenu / `menu` CI3**; submenu yang sudah di CI4 memakai tautan aktif di `layouts/sidebar.php`.
- Tombol primer diseragamkan ke **biru** (`bg-blue-600`) seperti aksen umum TailAdmin; sekunder tetap border gray + putih.
- Tabel: header `bg-gray-50`, border baris `divide-gray-100`, footer pagination dengan strip `bg-gray-50/80`.
