# Audit & Rekomendasi — Fitur Notifikasi

**Aplikasi:** LPK TIP v2 (Laravel 13 + Livewire 4 + Tailwind 4)
**Tanggal:** 2026-08-14 · **Status:** Audit ✅ · **Fase N1: SELESAI ✅** (N2–N3 lihat Roadmap)

---

## 1. Ringkasan Eksekutif

Fitur notifikasi saat ini **berstatus minimal / belum produktif**: hanya mampu *broadcast pengumuman manual* dari satu halaman, tidak ada notifikasi otomatis dari peristiwa bisnis, tidak ada mekanisme *read/unread* yang interaktif, dan **event `notify` yang sudah di-dispatch beberapa modul tidak di-handle sama sekali** (dead event). Rekomendasi: bangun fondasi notifikasi enterprise 3 fase — (N1) dasar produktif, (N2) interaksi penuh, (N3) real-time opsional.

---

## 2. Kondisi Saat Ini

### 2.1 Model `App\Models\Notification` (tabel `notifications`)
| Kolom | Fungsi |
|---|---|
| `user_id` (nullable) | `null` = broadcast ke semua user; terisi = notifikasi personal |
| `message` | Teks pesan (maks 500) |
| `is_read` / `read_at` | Status dibaca |
| `created_by` | Pengirim |

**Relasi:** `user()` (penerima), `creator()` (pengirim).

### 2.2 Produksi notifikasi
- **Satu-satunya pembuat**: `App\Livewire\Utilitas\Notifications::send()` — broadcast manual (`user_id = null`) dari menu *Sistem & Admin → Notifikasi*.
- **Dead event**: 3+ modul memanggil `$this->dispatch('notify', message: ...)` (mis. `SolarReceipts`, `SolarUsages`) **tetapi tidak ada listener global** → pesan tidak pernah tampil/masuk DB.

### 2.3 Konsumsi
- `App\Livewire\Topbar` — badge `unread` + 5 notif terbaru di dropdown (bukan real-time; hanya di-refresh saat halaman dimuat ulang).
- `App\Livewire\Utilitas\Notifications` — daftar semua (paginate 10) + form kirim.

---

## 3. Hasil Audit (Gap & Risiko)

### 🔴 Kritis
| # | Temuan | Dampak |
|---|---|---|
| A1 | `dispatch('notify')` **tidak memiliki listener** — event hilang | Notifikasi otomatis dari modul tidak pernah sampai |
| A2 | Tidak ada notifikasi otomatis dari peristiwa bisnis (SPB/Transfer/BKK/stok) | Nilai fitur ≈ 0 utk operasional |
| A3 | Tidak ada aksi *tandai dibaca* / *tandai semua* / *hapus* | Badge unread tidak pernah berkurang |

### 🟠 Sedang
| # | Temuan | Dampak |
|---|---|---|
| B1 | Tidak ada `type` (info/success/warning/danger) & `link` (navigasi) | Dropdown tidak informatif, tidak bisa klik ke dokumen |
| B2 | Tidak ada real-time / polling — unread hanya update saat reload | User tidak tahu ada notif baru |
| B3 | Tanpa otorisasi pengiriman broadcast — siapa pun berhak akses menu bisa kirim ke semua | Risiko spam/penyalahgunaan |
| B4 | Halaman notifikasi tanpa filter & bulk action | Pengelolaan manual berat |
| B5 | Tidak ada `icon`/`priority`/`expires_at`/payload JSON | Skalabilitas terbatas |

### 🟡 Ringan
| # | Temuan |
|---|---|
| C1 | Tidak ada relasi waktu relatif (sudah ada `diffForHumans` di view — OK) |
| C2 | Tidak ada grouping notifikasi per modul |
| C3 | `read_at` di-set tapi tidak pernah diisi |

---

## 4. Rekomendasi Arsitektur

### 4.1 Skema tabel (migrasi tambahan — backward compatible)
```
ALTER TABLE notifications
  ADD type        VARCHAR(20)  NULL DEFAULT 'info',   -- info|success|warning|danger
  ADD link        VARCHAR(255) NULL,                  -- route/navigasi (mis. /transfer/spb/12/show)
  ADD icon        VARCHAR(50)  NULL,                  -- id symbol menu-icons (mi-*)
  ADD payload     JSON         NULL,                  -- data tambahan
  ADD priority    TINYINT      NOT NULL DEFAULT 0,    -- 0 normal, 1 tinggi
  ADD expires_at  DATETIME     NULL;                  -- kedaluwarsa (opsional)
```

### 4.2 Notifier + event refresh (KEPUTUSAN IMPLEMENTASI — N1)
> ⚠️ **Temuan implementasi:** `Livewire::listen('notify', ...)` ternyata = `Event::listen` (Laravel Events) dan **TIDAK dipicu** oleh `$this->dispatch()` Livewire. Maka N1 memakai pola langsung:

```php
// di titik bisnis (save):
\App\Support\Notifier::send('SPB disimpan (MTRQ-...)', 'success', '/transfer/spb/12/show');
$this->dispatch('notify-refreshed'); // refresh badge Topbar
```

- `App\Support\Notifier::send(message, type, link, userId?, icon?)` — simpan ke DB (broadcast `user_id=null` atau personal).
- Topbar mendengar event `notify-refreshed` (`#[On('notify-refreshed')]`) → `refreshUnread()`.
- Modul lama yang masih memakai `dispatch('notify', ...)` (mis. `PayrollGenerate`, `Solar*`) **tidak lagi diteruskan** — akan dialihkan bertahap di N2.

### 4.3 Notifikasi otomatis dari peristiwa bisnis (hooks di `save()` tiap modul)
| Peristiwa | Penerima | Pesan contoh | Type/Link |
|---|---|---|---|
| SPB dibuat | Broadcast (tim gudang) | "SPB MTRQ-… dibuat" | info → show SPB |
| Material Transfer selesai | Broadcast | "MT TRMT-… selesai dari {gudang}" | success → index |
| BKK disimpan / dibatalkan | Broadcast + PIC proyek | "BKK LPK1-… Rp … (Proyek)" | warning (batal) → show |
| Stok menipis (check harian/saat simpan) | Broadcast | "{kode} {nama} stok {x} ≤ min {y}" | danger → /material |
| Absensi manual diinput | Personal (user tsb) | "Absensi 13/08 tersimpan" | success → /payroll/absensi |
| Pengumuman manual | Broadcast | sesuai admin | info |

### 4.4 Real-time — bertahap
1. **N1:** Polling `wire:poll.30s="refreshUnread"` di Topbar (tanpa infra tambahan).
2. **N3 (opsional):** Laravel Reverb + Echo — push event `notification.created` utk update badge instan.

### 4.5 UI
- **Dropdown topbar**: ikon per `type` + pesan (1 baris, truncate) + `diffForHumans` + **klik = tandai dibaca + navigasi ke `link`**; footer "Lihat semua".
- **Halaman Notifikasi**: filter (Semua / Belum dibaca), **Tandai semua dibaca**, per-item (Baca / Hapus), badge di menu, pagination enterprise (sesuai standar tabel).
- **Sidebar**: badge unread (sudah ada slot `badge` pada item menu).

### 4.6 Keamanan & otorisasi
- Broadcast hanya untuk role dengan otorisasi menu (sudah via `EnsureMenuAccess`); tambahan: tombol kirim broadcast hanya tampil utk user type Administrator.
- Validasi input: message ≤ 500, type whitelist, link whitelist prefix route.

---

## 5. Roadmap Implementasi

| Fase | Lingkup | Effort | Prioritas |
|---|---|---|---|
| **N1 — Fondasi produktif** ✅ | Migrasi kolom (type/link/icon/payload/priority/expires_at); `Notifier::send` + event `notify-refreshed`; auto-notify 5 peristiwa (SPB, Transfer, BKK, stok menipis via StockService, absensi personal); Topbar `wire:poll.30s` + dropdown klik → baca + navigasi; halaman notifikasi enterprise (filter, tandai semua, hapus, pagination); test 5 kasus | selesai | ⭐⭐⭐ |
| **N2 — Interaksi penuh** | Notifikasi per-user (user_id) utk aksi personal; badge unread real-time via Livewire event ke browser; grouping per modul; bulk delete | 1 hari | ⭐⭐ |
| **N3 — Real-time opsional** | Laravel Reverb + Echo (pusher lokal); preferensi notifikasi per user; ekspor CSV riwayat | 1–2 hari | ⭐ |

---

## 6. Acuan Kode

- Model: `app/Models/Notification.php`
- Topbar: `app/Livewire/Topbar.php` + `resources/views/livewire/topbar.blade.php`
- Halaman: `app/Livewire/Utilitas/Notifications.php` + `resources/views/livewire/utilitas/notifications.blade.php`
- Dead event yang di-listener ulang di N2: `PayrollGenerate.php:126`, `SolarReceipts.php:131`, `SolarUsages.php:141,147` (masih `dispatch('notify')`)
- Style dropdown: `x-cloak` + `x-transition` (sudah diterapkan)
- Standar tabel enterprise: `<x-table>` + `InteractsWithTable` (utk halaman notifikasi N1)

---

## 7. Kriteria Diterima (DoD)

- [x] `Notifier::send(...)` + event `notify-refreshed` tersimpan ke DB & badge update (tanpa reload)
- [x] Klik notifikasi → tandai dibaca + membuka dokumen terkait
- [x] Badge unread berkurang otomatis (polling 30 dtk + event refresh)
- [x] Halaman notifikasi: filter, tandai semua, hapus, pagination enterprise
- [ ] Otorisasi broadcast (admin) & validasi input — **N2**
- [x] Test otomatis (unit + feature) 5 kasus
