# Dokumentasi Perubahan — 28 Mei 2026

Proyek: INLISLite v3.3 (CodeIgniter 4)
Scope: Modul Eksemplar, User, Peminjaman, Pengembalian, Perpanjangan

---

## 1. EksemplarLabelController — Logika Warna Label

**File:** `app/Modules/SubModule/Eksemplar/Controllers/EksemplarLabelController.php`

### BEFORE
- Query `$eksemplarData` tidak mengambil kolom `DeweyNo`
- Logika pencocokan warna berupa pseudocode/komentar belum diimplementasikan
- Semua baris `master_kelas_besar` diambil tanpa filter, lalu difilter di PHP (tidak efisien)
- Kolom `Warna1` selalu bernilai default atau kosong

### AFTER
- Query `$eksemplarData` ditambah `b.DeweyNo`
- Ekstrak karakter pertama unik dari semua `DeweyNo` yang ada
- Query SQL berparameter: `WHERE LEFT(KdKelas, 1) IN (?)` — hanya ambil baris yang dibutuhkan
- Buat `$warnaMap` (array asosiatif `firstChar => Warna`) dari hasil query
- Per baris eksemplar: `'Warna1' => $warnaMap[$firstChar] ?? '#FFFF66'`

```php
// AFTER — snippet kunci
$firstChars = array_values(array_unique(array_filter(
    array_map(fn($row) => strtoupper(substr((string)($row->DeweyNo ?? ''), 0, 1)), $eksemplarData),
    fn($c) => $c !== ''
)));
if (!empty($firstChars)) {
    $placeholders = implode(',', array_fill(0, count($firstChars), '?'));
    $kelasRows = $db->query(
        "SELECT KdKelas, Warna FROM master_kelas_besar WHERE LEFT(KdKelas, 1) IN ($placeholders)",
        $firstChars
    )->getResultArray();
    foreach ($kelasRows as $kelas) {
        $key = strtoupper(substr((string)$kelas['KdKelas'], 0, 1));
        if (!isset($warnaMap[$key])) { $warnaMap[$key] = $kelas['Warna']; }
    }
}
```

---

## 2. Template Cetak Label — Warna Hardcoded

**File:** `app/Modules/SubModule/Eksemplar/Views/template/cetak-label-a4-3.php`

### BEFORE
```php
background-color: #013bad   // warna hardcoded biru
```

### AFTER
```php
background-color: <?= htmlspecialchars($label['Warna1']) ?>   // warna dinamis dari master_kelas_besar
```

---

## 3. User List — Tab "SEMUA"

**File:** `app/Modules/Core/User/Views/list.php`

### BEFORE
- Tab pertama adalah grup pertama (misal "Admin")
- Tidak ada cara menampilkan semua user sekaligus

### AFTER
- Tab "SEMUA" ditambahkan sebagai tab pertama
- Link: `base_url('user/index?slug=semua')`
- Tab aktif ketika `$slug === 'semua'`

```php
// AFTER — tab SEMUA
<li class="nav-item">
    <a class="nav-link <?= ($slug === 'semua') ? 'active' : '' ?>"
       href="<?= base_url('user/index?slug=semua') ?>">
        SEMUA
    </a>
</li>
```

---

## 4. User API Datatable — Filter slug=semua

**File:** `app/Modules/Core/User/Controllers/Api/User.php`

### BEFORE
```php
if (!empty($slug)) {
    $builder->like('a.category', $slug);
} else {
    $builder->where('id <', 0);  // tampilkan kosong jika slug tidak ada
}
```

### AFTER
```php
if ($slug === 'semua') {
    // tidak ada filter — tampilkan semua user
} elseif (!empty($slug)) {
    $builder->like('a.category', $slug);
} else {
    $builder->where('id <', 0);
}
```

---

## 5. Peminjaman — Cetak Struk (Print Sidebar)

**File:** `app/Modules/SubModule/Sirkulasi/Peminjaman/Views/success.php`

### BEFORE
- Tidak ada `@media print` CSS
- Saat `window.print()`, sidebar/header dari layout utama ikut tercetak

### AFTER
- Ditambahkan `@media print` CSS yang menyembunyikan komponen layout:

```css
@media print {
    .sidebar-argon, .argon-header, .mobile-menu-toggle, .app-page-title { display: none !important; }
    .app-main__outer { margin-left: 0 !important; padding: 0 !important; }
}
```

---

## 6. Peminjaman — Kirim Struk via Email

### BEFORE
- Tidak ada opsi pengiriman struk ke email anggota dari halaman success peminjaman

### AFTER — 3 file diubah:

**`app/Modules/SubModule/Sirkulasi/Peminjaman/Views/success.php`**
- Tombol "Kirim ke Email" ditambahkan
- JS function `kirimStrukEmail(loanId, email)` menggunakan fetch + SweetAlert2 confirm

**`app/Modules/SubModule/Sirkulasi/Peminjaman/Controllers/Peminjaman.php`**
- Method baru `sendStruk(int $loanId)`:
  - Query data loan + item dari DB
  - Panggil `$emailLib->sendStrukEmail($loan, $loanItems)`
  - Return JSON `{ success, message }`

**`app/Modules/SubModule/Sirkulasi/Peminjaman/Config/Routes.php`**
```php
// DITAMBAHKAN
$subroutes->post('send-struk/(:num)', 'Peminjaman::sendStruk/$1');
```

---

## 7. EmailNotificationLibrary — Method Baru

**File:** `app/Libraries/EmailNotificationLibrary.php`

### BEFORE
- Hanya ada method untuk notifikasi keterlambatan (`sendOverdueNotification`)

### AFTER — 6 method baru:

| Method | Keterangan |
|---|---|
| `sendStrukEmail(loan, items)` | Struk peminjaman ke email anggota |
| `buildStrukTemplate(loan, items)` | Template HTML struk peminjaman |
| `sendStrukPengembalianEmail(member, items)` | Struk pengembalian ke email anggota |
| `buildStrukPengembalianTemplate(member, items)` | Template HTML struk pengembalian (badge tepat waktu/terlambat) |
| `sendStrukPerpanjanganEmail(member, items)` | Struk perpanjangan ke email anggota |
| `buildStrukPerpanjanganTemplate(member, items)` | Template HTML struk perpanjangan (tema biru, tampil DueDateExtend) |

Semua method mengikuti pola yang sama:
```php
$this->email->initialize([...]);
$this->email->clear();
$this->email->setFrom(...)->setTo(...)->setSubject(...)->setMessage(...);
$this->email->send();
```

---

## 8. Pengembalian — Struk & Kirim Email

### BEFORE
- Setelah `processReturn()` sukses: hanya SweetAlert sukses, tidak ada halaman struk
- Tidak ada halaman success, tidak ada pengiriman email

### AFTER — 5 file diubah:

**`app/Modules/SubModule/Sirkulasi/Pengembalian/Controllers/Pengembalian.php`**
- `processReturn()`: tambah tracking `$totalDenda`, simpan session `struk_pengembalian`, sertakan `struk_url` di response
- Method baru `success()`: baca session → query items + member → render view
- Method baru `sendStruk()`: query items, panggil `sendStrukPengembalianEmail()`

**`app/Modules/SubModule/Sirkulasi/Pengembalian/Config/Routes.php`**
```php
// DITAMBAHKAN
$subroutes->get('success',    'Pengembalian::success');
$subroutes->post('send-struk','Pengembalian::sendStruk');
```

**`app/Modules/SubModule/Sirkulasi/Pengembalian/Views/add.php`** — JS handler sukses:
```js
// BEFORE
Swal.fire('Berhasil', message, 'success');

// AFTER
Swal.fire({
    title: 'Berhasil!', html: message, icon: 'success',
    showCancelButton: true,
    confirmButtonText: 'Lihat Struk',
    cancelButtonText: 'Tutup',
}).then(result => { if (result.isConfirmed) window.location.href = data.struk_url; });
```

**`app/Modules/SubModule/Sirkulasi/Pengembalian/Views/success.php`** *(FILE BARU)*
- Halaman struk pengembalian lengkap
- Informasi anggota + daftar buku dikembalikan
- Badge: "Tepat Waktu" (hijau) / "Terlambat X hari" (merah)
- Alert denda jika ada pelanggaran
- Tombol: Cetak Struk, Kirim ke Email, Pengembalian Baru, Kembali ke Beranda
- `@media print` CSS menyembunyikan sidebar/header
- JS: `kirimStrukEmail()` dengan SweetAlert confirm + fetch

---

## 9. Perpanjangan — Struk & Kirim Email

### BEFORE
- Setelah `processExtend()` sukses: hanya SweetAlert sukses, tidak ada halaman struk
- Tidak ada halaman success, tidak ada pengiriman email

### AFTER — 5 file diubah:

**`app/Modules/SubModule/Sirkulasi/Perpanjangan/Controllers/Perpanjangan.php`**
- `processExtend()`: simpan session `struk_perpanjangan`, sertakan `struk_url` di response
- Method baru `success()`: baca session → query collectionloanextends (MAX ID per item) → render view
- Method baru `sendStruk()`: query collectionloanextends, panggil `sendStrukPerpanjanganEmail()`

**`app/Modules/SubModule/Sirkulasi/Perpanjangan/Config/Routes.php`**
```php
// DITAMBAHKAN
$subroutes->get('success',    'Perpanjangan::success');
$subroutes->post('send-struk','Perpanjangan::sendStruk');
```

**`app/Modules/SubModule/Sirkulasi/Perpanjangan/Views/add.php`** — JS handler sukses:
```js
// BEFORE
Swal.fire('Berhasil', message, 'success');

// AFTER — sama seperti Pengembalian
Swal.fire({ showCancelButton: true, confirmButtonText: 'Lihat Struk', cancelButtonText: 'Tutup', ... })
.then(result => { if (result.isConfirmed) window.location.href = data.struk_url; });
```

**`app/Modules/SubModule/Sirkulasi/Perpanjangan/Views/success.php`** *(FILE BARU)*
- Halaman struk perpanjangan lengkap
- Informasi anggota + daftar buku diperpanjang
- Badge biru "Kembali: dd/mm/yyyy" menampilkan `DueDateExtend` baru
- Receipt box: Jumlah Buku, Durasi Perpanjangan, Tanggal
- Tombol: Cetak Struk, Kirim ke Email, Perpanjangan Baru, Kembali ke Beranda
- `@media print` CSS menyembunyikan sidebar/header
- JS: `kirimStrukEmail()` dengan SweetAlert confirm + fetch

---

## 10. Perpanjangan — Fix MySQL only_full_group_by

**File:** `app/Modules/SubModule/Sirkulasi/Perpanjangan/Controllers/Perpanjangan.php`
**Method:** `success()` dan `sendStruk()`

### BEFORE
```php
// Error: Expression #1 of SELECT list is not in GROUP BY clause
$items = $db->table('collectionloanextends as ce')
    ->select('ce.ID, ce.DateExtend, ce.DueDateExtend, ...')
    ->whereIn('ce.CollectionLoanItem_id', $itemIds)
    ->orderBy('ce.ID', 'DESC')
    ->groupBy('ce.CollectionLoanItem_id')  // ← melanggar only_full_group_by
    ->get()->getResult();
```

### AFTER
```php
// Langkah 1: ambil MAX(ID) per CollectionLoanItem_id
$maxIds = $db->table('collectionloanextends')
    ->select('MAX(ID) as max_id')
    ->whereIn('CollectionLoanItem_id', $itemIds)
    ->groupBy('CollectionLoanItem_id')
    ->get()->getResultArray();
$maxIdList = array_column($maxIds, 'max_id');

// Langkah 2: ambil data lengkap berdasarkan ID tersebut
$items = [];
if (!empty($maxIdList)) {
    $items = $db->table('collectionloanextends as ce')
        ->select('ce.ID, ce.DateExtend, ce.DueDateExtend, ...')
        ->join(...)
        ->whereIn('ce.ID', $maxIdList)
        ->get()->getResult();
}
```

**Root cause:** MySQL `sql_mode=only_full_group_by` melarang SELECT kolom non-agregat yang tidak ada di GROUP BY. Fix menggunakan subquery MAX(ID) untuk mendapatkan record terbaru per item tanpa melanggar aturan tersebut.

---

## Ringkasan File yang Diubah

| No | File | Status |
|---|---|---|
| 1 | `app/Modules/SubModule/Eksemplar/Controllers/EksemplarLabelController.php` | Diubah |
| 2 | `app/Modules/SubModule/Eksemplar/Views/template/cetak-label-a4-3.php` | Diubah |
| 3 | `app/Modules/Core/User/Views/list.php` | Diubah |
| 4 | `app/Modules/Core/User/Controllers/Api/User.php` | Diubah |
| 5 | `app/Modules/SubModule/Sirkulasi/Peminjaman/Views/success.php` | Diubah |
| 6 | `app/Modules/SubModule/Sirkulasi/Peminjaman/Controllers/Peminjaman.php` | Diubah |
| 7 | `app/Modules/SubModule/Sirkulasi/Peminjaman/Config/Routes.php` | Diubah |
| 8 | `app/Libraries/EmailNotificationLibrary.php` | Diubah |
| 9 | `app/Modules/SubModule/Sirkulasi/Pengembalian/Controllers/Pengembalian.php` | Diubah |
| 10 | `app/Modules/SubModule/Sirkulasi/Pengembalian/Config/Routes.php` | Diubah |
| 11 | `app/Modules/SubModule/Sirkulasi/Pengembalian/Views/add.php` | Diubah |
| 12 | `app/Modules/SubModule/Sirkulasi/Pengembalian/Views/success.php` | **BARU** |
| 13 | `app/Modules/SubModule/Sirkulasi/Perpanjangan/Controllers/Perpanjangan.php` | Diubah |
| 14 | `app/Modules/SubModule/Sirkulasi/Perpanjangan/Config/Routes.php` | Diubah |
| 15 | `app/Modules/SubModule/Sirkulasi/Perpanjangan/Views/add.php` | Diubah |
| 16 | `app/Modules/SubModule/Sirkulasi/Perpanjangan/Views/success.php` | **BARU** |
