# Rencana Optimasi Performa viewResume

Dokumen ini mencatat hasil diskusi dan analisis mengenai lambatnya pemuatan halaman *view resume* (hingga 25 detik) dan usulan solusi optimasi yang sesuai dengan *best practice*.

## Masalah Utama

1. **N+1 Query di Dalam Loop `foreach` (Pemicu Utama)**
   Di dalam controller `viewResume()`, terdapat loop untuk memproses data *Entry Point* (`$tmpEP`). Jika kolom `deskripsi` tidak kosong, sistem melakukan query database individual:
   ```php
   $tmpReg = $tr->lookupDataRegistriesByMasterID($row->link_id)->result();
   ```
   Hal ini menyebabkan pemanggilan query berulang kali (N+1 query) yang menambah *latency*.

2. **Query Join Berat pada Tabel `transaksi_data`**
   Pencarian relasi ke **AR Receipt (749)** dan **Faktur/Invoice (110)** menggunakan `lookupJoined_OLD()` melakukan `SELECT *` pada gabungan tabel `transaksi` dan `transaksi_data` (tabel dengan jutaan baris data) menggunakan pencarian filter `produk_id IN (...)`.

---

## Solusi Optimasi (Best Practice)

### 1. Batch Loading (Eager Loading) untuk Registry
Mengumpulkan semua `link_id` terlebih dahulu, kemudian menarik seluruh data registry yang dibutuhkan dalam **satu query tunggal** menggunakan operator `IN`. Hasil query disimpan ke dalam array map asosiatif di memori PHP agar lookup data di dalam loop `foreach` tidak perlu menyentuh database lagi.

### 2. Projection Select Column (Membatasi Kolom)
Mengganti penggunaan query generator umum `lookupJoined_OLD()` (yang melakukan `SELECT *` yang berat) dengan *select projection* yang hanya mengambil kolom-kolom yang diperlukan untuk rendering halaman resume.

---

## Rancangan Perubahan Kode

### Titik 1: Optimasi N+1 Query (Loop Registry)
**Sebelum:**
```php
else {
    $tmpReg = $tr->lookupDataRegistriesByMasterID($row->link_id)->result();
    $main = blobDecode($tmpReg[0]->main);
}
```

**Sesudah:**
```php
// 1. Kumpulkan semua link_id yang valid (Gunakan array_unique untuk efisiensi)
$batchLinkIds = array();
if (sizeof($tmpEP) > 0) {
    foreach ($tmpEP as $row) {
        if (!empty($row->deskripsi) && !empty($row->link_id)) {
            $batchLinkIds[] = $row->link_id;
        }
    }
}
$batchLinkIds = array_unique($batchLinkIds);

// 2. Ambil data registri sekaligus dengan Query Builder CI3 (Menghindari bug parsing filter model)
$registryMap = array();
if (sizeof($batchLinkIds) > 0) {
    $this->db->select("transaksi_id, main");
    $this->db->from("transaksi_registry");
    $this->db->where_in("transaksi_id", $batchLinkIds);
    $tmpBatchReg = $this->db->get()->result();
    if (sizeof($tmpBatchReg) > 0) {
        foreach ($tmpBatchReg as $eParams) {
            $registryMap[$eParams->transaksi_id] = $eParams;
        }
    }
}

// 3. Menggunakan data map memori PHP di dalam loop foreach
if (sizeof($tmpEP) > 0) {
    foreach ($tmpEP as $ii => $row) {
        ...
        else {
            $main = array();
            if (isset($registryMap[$row->link_id])) {
                $main = blobDecode($registryMap[$row->link_id]->main);
            }
        }
    }
}
```

### Titik 2: Optimasi Query Relasi AR
**Sebelum:**
```php
if (sizeof($terimaAr) > 0) {
    $trep = new MdlTransaksi();
    $trep->addFilter("transaksi.jenis='749'");
    $trep->addFilter("transaksi_data.produk_id in ('" . implode("','", $terimaAr) . "')");
    $trepTmp = $trep->lookupJoined_OLD()->result();
}
```

**Sesudah:**
```php
if (sizeof($terimaAr) > 0) {
    // Mendukung semua kode transaksi penerimaan (749, 1749, 2749, 7499)
    $list_jenis_penerimaan = array('749', '1749', '2749', '7499');
    
    $this->db->select("
        transaksi.id as transaksi_id, 
        transaksi.jenis, 
        transaksi.cabang_id, 
        transaksi.id_master, 
        transaksi.counters, 
        transaksi.step_number, 
        transaksi.jenis_master, 
        transaksi.cabang_nama, 
        transaksi.jenis_label, 
        transaksi.dtime, 
        transaksi.nomer, 
        transaksi.oleh_nama, 
        transaksi.deskripsi, 
        transaksi.link_id
    ");
    $this->db->from("transaksi");
    $this->db->join("transaksi_data", "transaksi_data.transaksi_id = transaksi.id");
    $this->db->where_in("transaksi.jenis", $list_jenis_penerimaan);
    $this->db->where_in("transaksi_data.produk_id", $terimaAr);
    $this->db->where("transaksi_data.trash", "0");
    $this->db->where("transaksi.trash", "0");
    $trepTmp = $this->db->get()->result();
}
```

### Titik 3: Optimasi Query Relasi Faktur
**Sebelum:**
```php
if (sizeof($inputFaktur) > 0) {
    $trep = new MdlTransaksi();
    $trep->addFilter("transaksi.jenis_master='110'");
    $trep->addFilter("transaksi_data.produk_id in ('" . implode("','", $inputFaktur) . "')");
    $trepTmp = $trep->lookupJoined_OLD()->result();
}
```

**Sesudah:**
```php
if (sizeof($inputFaktur) > 0) {
    $this->db->select("
        transaksi.id as transaksi_id, 
        transaksi.jenis, 
        transaksi.cabang_id, 
        transaksi.id_master, 
        transaksi.counters, 
        transaksi.step_number, 
        transaksi.jenis_master, 
        transaksi.cabang_nama, 
        transaksi.jenis_label, 
        transaksi.dtime, 
        transaksi.nomer, 
        transaksi.oleh_nama, 
        transaksi.deskripsi, 
        transaksi.link_id, 
        transaksi.efaktur_dtime, 
        transaksi.efaktur, 
        transaksi.gunggungan_mode, 
        transaksi.trash_4, 
        transaksi.cancel_dtime, 
        transaksi.cancel_transaksi_nomer, 
        transaksi.cancel_name
    ");
    $this->db->from("transaksi");
    $this->db->join("transaksi_data", "transaksi_data.transaksi_id = transaksi.id");
    $this->db->where("transaksi.jenis_master", "110");
    $this->db->where_in("transaksi_data.produk_id", $inputFaktur);
    $this->db->where("transaksi_data.trash", "0");
    $this->db->where("transaksi.trash", "0");
    $trepTmp = $this->db->get()->result();
}
```

---

## Analisis & Pencegahan Potensi Bug Tambahan

### 1. Masalah Parsing Query Filter Bawaan Model CI3
*   **Masalah**: Penggunaan kustomisasi filter model seperti `$tr_batch->addFilter("transaksi_id in (...)")` berisiko memicu kegagalan query SQL karena keterbatasan parser filter string internal model.
*   **Solusi**: Menggunakan **Query Builder / Active Record CI3 secara langsung** dari controller (`$this->db->select()->from()->where_in()->get()`). Pendekatan ini terbukti stabil dan menghindari bypass logic model.

### 2. Duplikasi data di `$batchLinkIds`
*   **Masalah**: Jika satu transaksi memiliki beberapa entry point yang menunjuk ke `link_id` yang sama, query pencarian registry akan diproses berulang-ulang untuk nilai ID yang sama.
*   **Solusi**: Menggunakan `array_unique($batchLinkIds)` sebelum memproses array tersebut di query `where_in`.

### 3. Keamanan Indeks saat Membuka Halaman Spesifik
*   **Masalah**: Meskipun query mendukung semua jenis transaksi penerimaan (`749`, `1749`, `2749`, `7499`), data yang diambil tetap dibatasi oleh produk/item yang terkait secara langsung via `where_in("transaksi_data.produk_id", $terimaAr)`.
*   **Solusi**: Hal ini memastikan database tidak melakukan pemindaian data yang tidak perlu (Full Table Scan) dan beban kueri tetap terlokalisasi di memori PHP.

### 4. Optimasi Kueri Jurnal via ID Numerik (Modul Penerimaan)
*   **Masalah**: Fungsi `viewResume()` pada modul `penerimaan` mengalami kelambatan karena pencarian jurnal menggunakan filter string `transaksi_no = '$no'` pada tabel `jurnal` yang memiliki jutaan baris data tanpa indeks yang memadai.
*   **Solusi**: Mengubah filter pencarian jurnal menjadi berbasis ID transaksi numerik (`transaksi_id = '$transaksiID'`). Kolom kunci asing numerik (`transaksi_id`) secara default telah memiliki indeks database (Foreign Key Index), sehingga eksekusi pencarian berjalan sangat instan (di bawah 10ms) dan memangkas waktu pemuatan halaman secara drastis di bawah 1-2 detik.

