# Rekomendasi — Log Absensi Web: Grouping per Tanggal per Karyawan

**Aplikasi:** LPK TIP v2 (Web Laravel 13 + Livewire 4 + Tailwind)
**Modul:** Payroll → Log Absensi (`/payroll/log-absensi`)
**Tanggal:** 2026-08-24 · **Status:** ✅ Terimplementasi & teruji

---

## 1. Latar Belakang & Masalah

Tabel **Log Absensi** sebelumnya menampilkan **1 baris = 1 log check-in/out**:

| Tanggal | NIK | Nama | Tipe | Jam | Lokasi | … |
|---|---|---|---|---|---|---|
| 19/08 | 001 | Budi | Masuk | 08:00 | Proyek A | … |
| 19/08 | 001 | Budi | Keluar | 17:05 | Proyek A | … |
| 19/08 | 002 | Ani | Masuk | 07:55 | Proyek B | … |

**Masalah:**
- **1 hari kerja karyawan terpecah menjadi 2 baris** (Masuk & Keluar) — sulit dibaca & sulit dibandingkan dengan hasil **Export Excel** yang sudah benar (1 baris per hari dengan kolom Check In & Check Out berdampingan).
- Tidak konsisten antara tampilan web dan file export → menyulitkan verifikasi data oleh admin payroll.
- Edit/koreksi jam masuk & jam keluar dilakukan terpisah (2x klik, 2x simpan).

**Tujuan:** Menyamakan tampilan web dengan hasil export — **1 baris = 1 hari kerja karyawan**, jam masuk & jam keluar dalam **2 kolom berdampingan**.

---

## 2. Desain Baru

### 2.1 Kunci grouping

```
GROUP BY (employee_id, tanggal date_in)
```

- **1 baris tabel = 1 hari kerja karyawan** (bukan 1 log).
- Log `attendance_type = 'in'` → kolom **Jam Masuk**; `attendance_type = 'out'` → kolom **Jam Keluar**.
- Jika hanya ada 1 tipe (mis. baru masuk, belum keluar), kolom pasangannya tampil `—`.

### 2.2 Kolom tabel baru

| # | Kolom | Sumber |
|---|---|---|
| 1 | **Tanggal** | `date_in` (format `d/m/Y`) |
| 2 | **NIK** | `employee.nik` |
| 3 | **Nama** | `employee.name` |
| 4 | **Shift** | `employee.shift.name` |
| 5 | **Proyek** | `project.name` dari log hari tsb |
| 6 | **Jam Masuk** | `actual_in` dari log tipe `in` + sub-info: lokasi, akurasi (m), badge `Mock!`; **icon peta → modal Google Maps** (koordinat, akurasi, tombol buka di Maps) |
| 7 | **Jam Keluar** | `actual_out` dari log tipe `out` + sub-info sama |
| 8 | **Sumber** | kombinasi `is_mobile` log masuk & keluar (`Mobile`, `Manual`, `Mobile/Manual`) |
| 9 | **Foto Selfie** | **2 thumbnail**: foto selfie **Masuk** & **Keluar**; **klik thumbnail langsung membuka modal Perbandingan Wajah** (selfie vs wajah terdaftar); fallback perbesar bila wajah belum terdaftar |
| 10 | **Aksi** | **Edit hari** · **Hapus hari** |

### 2.3 Urutan & pagination

- Urutan default: **tanggal terbaru → terlama**, lalu **nama karyawan A→Z**.
- Pagination dihitung per **grup (hari-karyawan)**, bukan per log — konsisten dengan info baris "Menampilkan 1–8 dari 8 data".
- Semua filter lama tetap berlaku: rentang tanggal, tipe (Masuk/Keluar), sumber (Mobile/Manual), proyek, staff/TK, pencarian NIK/nama.

---

## 3. Perubahan Kode

### 3.1 Komponen `app/Livewire/Payroll/AbsensiLog.php`

| Method | Sebelum | Sesudah |
|---|---|---|
| `render()` | paginate per log (`Attendance::paginate`) | **`groupedRows()`** → `LengthAwarePaginator` per grup; tambahan `$groupCount` & `$logCount` untuk header |
| `groupedRows()` | — | Baru: `get()` semua log hasil filter → `groupBy(employee_id|date)` → map 1 baris per hari (jam masuk/keluar + metadata) → sort tanggal desc, nama asc |
| `logCell()` | — | Baru: ringkas 1 log → `{id, time, loc, acc, is_mobile, is_mocked, lat, lon}` |
| `openEdit()` / `saveEdit()` | edit **1 log** (pilih tipe Masuk/Keluar) | Diganti `openDay(inId, outId)` & `saveDay(inId, outId, employeeId, date, in, out, present)` — **edit 1 hari sekaligus** |
| `delete(id)` | hapus 1 log | Dipertahankan (kompatibilitas) + baru `deleteDay(employeeId, date)` — hapus **seluruh hari** (masuk & keluar) |
| `exportCsv()` / `exportExcel()` | — | **Tidak berubah** (ekspor tetap per log / per hari sesuai template lama) |

**Semantik `saveDay`:**
- Jam masuk diisi → *upsert* log tipe `in` (update record `inId` bila ada, atau buat baru).
- Jam keluar diisi → *upsert* log tipe `out`.
- Salah satu jam dikosongkan → log tipe tersebut **dihapus** (jika ada).
- Kedua jam kosong → **ditolak** (pesan: minimal 1 jam wajib diisi) — mencegah penghapusan tidak sengaja via form.
- Semua perubahan ditandai `is_manual = 1`.

### 3.2 View `resources/views/livewire/payroll/absensi-log.blade.php`

- Tabel dirombak dari kolom `Tipe`+`Jam` menjadi 2 kolom jam berdampingan dengan sub-info lokasi/akurasi.
- Modal edit diubah menjadi **2 input jam (Masuk & Keluar)** + tanggal + karyawan + hadir — 1 simpan untuk 1 hari.
- Tombol aksi: edit hari (`editDay`) & hapus hari (`hapusDay` dengan konfirmasi berisi nama karyawan + tanggal).
- Header menampilkan jumlah hari & log: `X hari · Y log`.

---

## 4. Perilaku & Kasus Tepi

| Kasus | Perilaku |
|---|---|
| Hanya log `in` (belum keluar) | Baris tetap tampil; kolom Jam Keluar `—`; foto/verifikasi wajah Keluar `—` |
| Hanya log `out` | Baris tetap tampil; kolom Jam Masuk `—` |
| Filter tipe = Masuk | Semua baris hanya berisi kolom Jam Masuk; Jam Keluar `—` |
| >1 log `in` sehari (duplikat) | Diambil `first` log masuk (urutan query) — perilaku sama dengan export |
| Akurasi > 100 m | Warna merah peringatan pada sub-info akurasi |
| Log mock (GPS palsu) | Badge merah `Mock!` |
| Edit pindahkan tanggal/karyawan | Kedua log ikut dipindah (upsert di record yang sama) |
| Hapus hari | Semua `attendances` karyawan tsb pada tanggal tsb dihapus (konfirmasi jelas) |
| Memory data besar | Sama dengan export (`get()` + group PHP) — rentang tanggal default 1 bulan; untuk rentang sangat lebar disarankan filter |

---

## 5. Test

**File:** `tests/Feature/AbsensiLogEditTest.php` (diperbarui) + `AbsensiLogExportTest.php` (tidak berubah)

| Test | Kasus |
|---|---|
| `test_open_day_save_day_and_delete_day` | openDay mengembalikan data hari (in/out) → saveDay mengubah kedua jam & tanggal → validasi keduanya kosong ditolak → deleteDay menghapus masuk & keluar |
| `test_table_renders_one_grouped_row_per_employee_day` | 2 log (in+out) karyawan-tanggal sama → 1 grup; header `1 hari · 2 log`; kedua jam tampil |
| `test_open_day_save_and_delete_single_log` | `delete(id)` kompatibilitas tetap jalan |

Hasil: **AbsensiLog 3/3 + Export 1/1 lulus**. Full suite: **138/140 lulus** (1 kegagalan `PeriodeBadgeTest` **pre-existing**, tidak terkait — assertion `>1</td>` rapuh terhadap data lain di DB test).

---

## 6. Rekomendasi Lanjutan (opsional)

1. **Sorting kolom** — jadikan header Tanggal/Nama/Shift dapat di-sort (asc/desc) via `InteractsWithTable::sortBy` (saat ini urutan tetap).
2. **Kolom "Jam jadi" (masuk/keluar menjadi)** — tambahkan kolom perhitungan LPK (seperti export kolom 10–11) langsung di tabel web untuk verifikasi cepat admin.
3. **Pagination server-side murni** — untuk rentang data sangat besar, pindahkan grouping ke SQL (`MIN/MAX` + join) agar tidak `get()` semua baris.
4. **Edit inline** — ubah jam langsung di sel tabel (click-to-edit) dengan debounce, tanpa modal.
5. **Batas jumlah log per grup** — bila satu karyawan punya >1 pasang in/out sehari, tampilkan baris kedua dengan tanda "+N" daripada `first` saja.

---

## 7. Kesimpulan

Perubahan ini **menyamakan tampilan web Log Absensi dengan hasil Export Excel** (1 baris = 1 hari kerja karyawan, jam masuk & jam keluar berdampingan), mempercepat koreksi data admin (1 simpan untuk 1 hari), dan mempertahankan seluruh filter, ekspor, serta logika bisnis yang ada. Risiko regresi rendah — seluruh alur transaksi & payroll lain tidak tersentuh.
