# 📘 BUKU PANDUAN LENGKAP & BLUEPRINT MIGRASI SERVER-SIDE DATATABLES (OPT-IN SWITCH) EVEREST ERP

> **DOKUMEN INI ADALAH SINGLE SOURCE OF TRUTH (SSOT) UNTUK SEMUA AI AGENT.**  
> Dokumen ini memuat seluruh hasil analisa codebase, arsitektur, pemetaan kolom endemik, penanganan kustom, serta template kode siap pakai. **AI Agent selanjutnya TIDAK PERLU melakukan scanning codebase dari awal.** Cukup ikuti SOP pada dokumen ini.

---

## 1. Latar Belakang & Masalah Arsitektur Lama (Client-Side History)

Halaman **History Transaksi** tersebar di puluhan modul (`penjualan`, `pembelian`, `invoicing`, `distribusifg`, `penerimaan`, `kas`, dll.).

### Masalah pada Arsitektur Lama:
1. **High Latency & Risiko Memory Exhaustion:**
   - Controller lama mengambil hingga 100+ baris transaksi sekaligus menggunakan `MdlTransaksi`.
   - Looping PHP melakukan *unserializing blob*, format tanggal, kalkulasi finansial, dan pemanggilan helper per baris data.
   - Akibatnya, waktu muat halaman mencapai 3–10 detik per pergantian tab/tanggal.
2. **Kolom Kustom & Endemik yang Kompleks:**
   - **Kolom Status (`status_bayar` / `status_next`):** Tidak ada di tabel `transaksi`. Dihitung dinamis dari `transaksi_payment_source` dan riwayat pelunasan invoice.
   - **Kolom Isi (`item_fields`):** Berisi rincian mini-tabel produk (SKU, barcode, nama barang, qty) yang disimpan di `transaksi_data_registry` (step 9: `items` dan `items2`).
   - **Kolom Step & Key (`ids_his`):** Berisi nomor dokumen atau aktor dari langkah tertentu (misal: `nomer_soa`, `pengirim_nama`, `worker_nama`, `seller_nama`) yang disimpan dalam blob JSON `ids_his`.
   - **Custom Buttons:** Tombol export Excel kustom (`customButton`), cetak nota valas (`print_nvalas`), dll.
3. **Pencarian Client-Side Lambat:**
   - DataTables client-side hanya bisa mencari data yang sudah di-render di halaman saat itu, tidak bisa mencari data di luar batasan query PHP tanpa me-load ribuan data ke DOM.

---

## 2. Solusi Strategis: "Opt-In Switch" (Feature Flag Per User)

Untuk menghindari risiko komplain operasional dan mempermudah pengujian di lapangan, diterapkan mekanisme **Canary / Feature Flag Per User**:

```
+-----------------------------------------------------------------------------------+
|                                 BROWSER (USER)                                    |
|                                                                                   |
|  [ Tombol: 🚀 Coba Tampilan Baru ]  <--->  [ Tombol: ⏮️ Kembali Ke Tampilan Lama ] |
+------------------------------------------+----------------------------------------+
                                           |
                                           v
                 +---------------------------------------------------+
                 |       application/modules/{modul}/controllers/    |
                 |                     History.php                   |
                 |                                                   |
                 |  1. Cek Cookie & Session:                         |
                 |     - Jika '1' -> viewHistoryServerSide()         |
                 |     - Jika '0' -> viewHistoryLegacy()             |
                 |                                                   |
                 |  2. Endpoint toggle: toggleHistoryMode()          |
                 |     - Set cookie `pref_history_serverside_{UID}`  |
                 |     - Set $_SESSION['login']['pref_history_...']  |
                 +-------------------+-------------------------------+
                                     |
                +--------------------+--------------------+
                |                                         |
                v                                         v
   +--------------------------+             +-------------------------------+
   |   TAMPILAN LAMA (LEGACY) |             |    TAMPILAN BARU (SERVER-SIDE)|
   |   - viewHistoryLegacy    |             |    - DatatablesHistory.php    |
   |   - Client-side data     |             |    - Endpoint showDataAjax    |
   |   - Loop $arrayHistory   |             |    - Multi-keyword search     |
   |   - Paging lokal         |             |    - SARGable SQL indexing    |
   +--------------------------+             |    - Native Endemic Handlers  |
                                            +-------------------------------+
```

### Keuntungan Mekanisme Ini:
1. **Nol Risiko Downtime:** Pengguna yang sedang sibuk transaksi tidak terganggu.
2. **Instant Self-Service Rollback:** Jika ada kolom kustom di modul tertentu yang belum ter-render sempurna, user cukup mengklik tombol **"Kembali Ke Tampilan History Lama"** (1 detik).
3. **Persisten:** Preferensi disimpan di Cookie Browser selama **1 tahun** (`time() + (365 * 86400)`) dan di-cache di `$_SESSION['login']`.

---

## 3. Hasil Analisis Menyeluruh Codebase History

### 3.1 Struktur File & Tanggung Jawab
Setiap modul transaksi memiliki pasangan file history:
1. **Controller:** `application/modules/{modul}/controllers/History.php` (extends `Modul_Controller` -> `MX_Controller`).
2. **View:** `application/modules/{modul}/views/history.php`.
3. **Core Engine:** `application/libraries/DatatablesHistory.php` (singleton library yang dipakai bersama oleh semua modul).

### 3.2 Anatomi Controller `History.php`
Di dalam controller `History.php` pada setiap modul:
- `viewHistory($jenisTr, $jenisTrsub)`:
  - Dipanggil saat user membuka halaman history utama via URL: `{base}{modul}/History/viewHistory/{jenisTr}/{jenisTrsub}`.
  - Berfungsi sebagai **Dispatcher**. Membaca cookie/sesi preferensi:
    - Jika `'1'` -> panggil `viewHistoryServerSide()`.
    - Jika `'0'` -> panggil `viewHistoryLegacy()`.
  - Melempar parameter ke view: `"mode" => "viewHistory"` (atau `$this->uri->segment(3)`).
- `showData($jenisTr, $jenisTrsub)`:
  - Dipanggil saat AJAX partial tab load (`$('#historyList').load('$linkStep')`).
  - Berfungsi sebagai **Dispatcher**:
    - Jika `'1'` -> panggil `showDataServerSide()`.
    - Jika `'0'` -> panggil `showDataLegacy()`.
  - Melempar parameter ke view: `"mode" => "showData"`.
- `showDataAjax($jenisTr, $currentState)`:
  - Endpoint AJAX yang dipanggil oleh DataTables Server-Side (`table.DataTable({ ajax: ... })`).
  - Menginstansiasi `DatatablesHistory` dan mengeksekusi query server-side.
- `toggleHistoryMode()`:
  - Endpoint untuk beralih mode. Menerima `$_GET['mode']` (`'new'` atau `'old'`) dan `$_GET['redirect']`.

### 3.3 Anatomi View `history.php`
Di dalam view `history.php`:
- `switch ($mode)`:
  - `case "viewStatus":` / `case "viewHistory":`:
    - Merender template lengkap dengan header layout Everest ERP:
      `$p = New Layout("$title", "$subTitle", MODUL_TEMPLATE_PATH . "/template/history.html");`
      `$p->render();`
    - Memiliki dua cabang tabel:
      - Jika `$isServerSide == true`: Merender tabel dengan `<tbody>` kosong, paging 20, dan AJAX DataTables ke `showDataAjax`.
      - Jika `$isServerSide == false`: Merender tabel legacy dengan `<tbody>` berisi loop PHP dari `$arrayHistory`.
  - `case "showData":`:
    - Hanya meng-echo `$content` (tanpa `New Layout`), digunakan saat navigasi tab via AJAX.
    - Juga memiliki dua cabang: `$isServerSide == true` vs `$isServerSide == false`.

---

## 4. Bedah Masalah & Solusi Kolom Kustom/Endemik (Crucial Knowledge Base)

Berikut adalah ringkasan teknis mengapa kolom kustom sempat gagal dan bagaimana engine `DatatablesHistory.php` mengatasinya:

### 4.1 Kolom Status Pembayaran (`status_bayar` / `status_next`)
- **Penyebab Gagal:** Kolom ini **bukan** kolom di tabel `transaksi`. Nilainya dihitung dinamis dari relasi tabel pembayaran (`transaksi_payment_source`), sisa piutang, dan termin pembayaran.
- **Solusi Standar di `DatatablesHistory.php`:**
  Engine memuat helper finansial dan memanggil:
  ```php
  $this->ci->load->helper('he_finansial');
  $status = fin_get_payment_status($row->id, 1000.0, true);
  ```
  Helper ini menghasilkan badge HTML resmi Everest ERP:
  - `🟢 LUNAS` (badge success)
  - `🟠 CICILAN` (badge warning)
  - `🟡 BELUM LUNAS` (badge danger / neutral)

### 4.2 Kolom Isi Rincian Barang (`item_fields`)
- **Penyebab Gagal:** Data rincian barang tidak ada di tabel `transaksi`, melainkan tersimpan dalam database MongoDB / `transaksi_data_registry` pada langkah 9 (`items` dan `items2`).
- **Solusi Standar di `DatatablesHistory.php`:**
  Engine mendeteksi apakah kolom berlabel `item_fields` ada di konfigurasi UI:
  ```php
  // Ambil data items dari transaksi_data_registry jika kolom isi diminta
  $reg = $this->ci->db->select('dataDoc')
      ->where('transaksi_id', $row->id)
      ->where_in('step', array(9, 'items', 'items2'))
      ->get('transaksi_data_registry')
      ->row();
  ```
  Lalu dirender menggunakan fungsi bawaan `viewDetailTransaksi()` dengan fallback HTML tabel mini (SKU, Barcode, Nama Produk, Qty, Satuan).

### 4.3 Kolom Relasi Antar-Langkah (`ids_his`)
- **Penyebab Gagal:** Di konfigurasi UI, kolom sering didefinisikan sebagai array bertingkat: `array('step' => 2, 'key' => 'nomer_soa')` atau `array('step' => 3, 'key' => 'pengirim_nama')`.
- **Solusi Standar di `DatatablesHistory.php`:**
  Engine melakukan parsing terhadap blob JSON `ids_his`:
  ```php
  $idsHis = !empty($row->ids_his) ? blobDecode($row->ids_his) : array();
  if (is_array($colDef) && isset($colDef['step']) && isset($colDef['key'])) {
      $val = isset($idsHis[$colDef['step']][$colDef['key']]) ? $idsHis[$colDef['step']][$colDef['key']] : '';
      // Fallback ke atribut objek jika tidak ditemukan di ids_his
      if (empty($val) && isset($row->{$colDef['key']})) {
          $val = $row->{$colDef['key']};
      }
  }
  ```

### 4.4 Hook Filter Endemik Modul (`setFilterHook`)
Setiap modul dapat menyuntikkan filter database tambahan sebelum query dijalankan:
- **Contoh di Penjualan:** Freelance salesperson (`employee_freelance`) hanya boleh melihat data transaksi miliknya:
  ```php
  if (isset($this->session->login['employee_type']) && $this->session->login['employee_type'] == "employee_freelance") {
      $sellerId = $this->session->login['id'];
      $this->datatableshistory->setFilterHook(function($db) use ($sellerId) {
          $db->where('transaksi.seller_id', $sellerId);
      });
  }
  ```

---

## 5. SOP & Template Kode Migrasi Modul (Step-by-Step Guide)

Setiap AI Agent yang bertugas memigrasi modul baru WAJIB mengikuti 3 langkah standar berikut:

### 📋 LANGKAH 1: Perbarui Controller `History.php`
Buka file `application/modules/{nama_modul}/controllers/History.php`:

1. **Tambahkan Endpoint `toggleHistoryMode()`:**
   ```php
   public function toggleHistoryMode()
   {
       // START OF COMPLETE REPEATED LOGIC
       if (!isset($this->session->login['id'])) {
           gotoLogin();
       }
       $userId = $this->session->login['id'];
       $targetMode = isset($_GET['mode']) ? $_GET['mode'] : 'new';
       $redirectUrl = isset($_GET['redirect']) ? $_GET['redirect'] : '';

       $cookieVal = ($targetMode === 'new') ? '1' : '0';
       setcookie("pref_history_serverside_" . $userId, $cookieVal, time() + (365 * 86400), "/");
       if (isset($_SESSION['login'])) {
           $_SESSION['login']['pref_history_serverside'] = $cookieVal;
       }

       if (!empty($redirectUrl)) {
           redirect($redirectUrl);
       } else {
           redirect(MODUL_PATH . "History/viewHistory/" . $this->jenisTr);
       }
       // END OF COMPLETE REPEATED LOGIC
   }
   ```

2. **Ubah `viewHistory()` Menjadi Dispatcher:**
   ```php
   public function viewHistory()
   {
       // START OF COMPLETE REPEATED LOGIC
       if (!isset($this->session->login['id'])) {
           gotoLogin();
       }
       $userId = $this->session->login['id'];
       $prefKey = "pref_history_serverside_" . $userId;
       $isServerSide = false;

       if (isset($_SESSION['login']['pref_history_serverside'])) {
           $isServerSide = ($_SESSION['login']['pref_history_serverside'] == '1');
       } elseif (isset($_COOKIE[$prefKey])) {
           $isServerSide = ($_COOKIE[$prefKey] == '1');
           $_SESSION['login']['pref_history_serverside'] = $_COOKIE[$prefKey];
       }

       if ($isServerSide) {
           return $this->viewHistoryServerSide();
       } else {
           return $this->viewHistoryLegacy();
       }
       // END OF COMPLETE REPEATED LOGIC
   }
   ```
   *(Catatan: Fungsi lama `viewHistory` diganti nama menjadi `viewHistoryLegacy()`, dan ditambahkan `"isServerSide" => false` di array `$data`-nya).*

3. **Buat Fungsi `viewHistoryServerSide()`:**
   Fungsi ini menyalin persiapan UI (labels, steps, links, dates) dari fungsi legacy, namun:
   - **TIDAK** menjalankan query transaksi via `MdlTransaksi`.
   - Mengisi `"arrayHistory" => array()`.
   - Menambahkan `"isServerSide" => true`.
   - Menetapkan `"mode" => $this->uri->segment(3)` (yaitu `"viewHistory"`).

4. **Ubah `showData()` Menjadi Dispatcher:**
   ```php
   public function showData()
   {
       $userId = isset($this->session->login['id']) ? $this->session->login['id'] : 0;
       $prefKey = "pref_history_serverside_" . $userId;
       $isServerSide = false;

       if (isset($_SESSION['login']['pref_history_serverside'])) {
           $isServerSide = ($_SESSION['login']['pref_history_serverside'] == '1');
       } elseif (isset($_COOKIE[$prefKey])) {
           $isServerSide = ($_COOKIE[$prefKey] == '1');
           $_SESSION['login']['pref_history_serverside'] = $_COOKIE[$prefKey];
       }

       if ($isServerSide) {
           return $this->showDataServerSide();
       } else {
           return $this->showDataLegacy();
       }
   }
   ```
   *(Fungsi lama `showData` diganti nama menjadi `showDataLegacy()`, dan fungsi baru `showDataServerSide()` dibuat tanpa query berat).*

5. **Tambahkan Endpoint `showDataAjax()`:**
   ```php
   public function showDataAjax()
   {
       // START OF COMPLETE REPEATED LOGIC
       if (!isset($this->session->login['id'])) {
           $response = array(
               "draw"            => 0,
               "recordsTotal"    => 0,
               "recordsFiltered" => 0,
               "data"            => array(),
               "error"           => "Session expired",
           );
           $this->output
               ->set_content_type('application/json')
               ->set_output(json_encode($response));
           return;
       }

       if (!class_exists('DatatablesHistory', false)) {
           require_once APPPATH . 'libraries/DatatablesHistory.php';
       }
       $this->datatableshistory = new DatatablesHistory();

       $jenisTr = !empty($this->jenisTr) ? $this->jenisTr : $this->uri->segment(4);
       $this->datatableshistory->init($jenisTr, $this->configUi, $this->configLayout, $this->placeId);

       // Tambahkan filter hook jika modul memiliki filter endemik
       // $this->datatableshistory->setFilterHook(function($db) { ... });

       $this->datatableshistory->execute();
       // END OF COMPLETE REPEATED LOGIC
   }
   ```

---

### 📋 LANGKAH 2: Perbarui View `views/history.php`
Buka file `application/modules/{nama_modul}/views/history.php`:

1. **Inisialisasi Tombol Switch di Awal `switch ($mode)`:**
   ```php
   $isServerSide = (isset($isServerSide) && $isServerSide) ? true : false;
   $currentFullUrl = current_url() . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
   if ($isServerSide) {
       $switchUrl = base_url() . "{nama_modul}/History/toggleHistoryMode?mode=old&redirect=" . urlencode($currentFullUrl);
       $btnSwitchHistory = "<a href='$switchUrl' class='btn btn-sm btn-warning' title='Beralih kembali ke tampilan tabel lama' style='font-weight: bold; border-radius: 4px; padding: 4px 10px; margin-bottom: 2px;'><i class='fa fa-backward'></i> Kembali Ke Tampilan History Lama</a>";
   } else {
       $switchUrl = base_url() . "{nama_modul}/History/toggleHistoryMode?mode=new&redirect=" . urlencode($currentFullUrl);
       $btnSwitchHistory = "<a href='$switchUrl' class='btn btn-sm btn-success' title='Coba tampilan baru yang lebih cepat dengan server-side paging & pencarian instan' style='font-weight: bold; border-radius: 4px; padding: 4px 10px; margin-bottom: 2px;'><i class='fa fa-rocket'></i> Coba Tampilan History Baru</a>";
   }
   ```

2. **Pasang Tombol di Tab Navigasi & Header Box:**
   - Di tab bar:
     ```php
     $content .= ("<li class='pull-right' style='padding-top: 4px; padding-right: 5px;'>$btnSwitchHistory</li>");
     ```
   - Di header box:
     ```php
     $contentStr .= "<div class='box-header with-border'>
                         <span class='pull-left text-uppercase'><h4 class='no-padding no-margin'>$title</h4></span>
                         <span class='box-tools pull-right'>$btnSwitchHistory &nbsp; $strBg</span>
                     </div>";
     ```

3. **Percabangan Render Tabel:**
   Gunakan `if ($isServerSide) { ... } else { ... }`:
   - **Jika `$isServerSide == true`:**
     - Output `<thead>` dari `$arrayHistoryLabels`.
     - Output `<tbody></tbody>` kosong.
     - Init DataTables dengan `serverSide: true, ajax: '{base}{modul}/History/showDataAjax/$jenisTr/$currentState'`.
   - **Jika `$isServerSide == false`:**
     - Output tabel legacy yang melakukan perulangan `foreach ($arrayHistory as $ii => $row)`.

---

### 📋 LANGKAH 3: Validasi Sintaks PHP 5.6 CLI
Setelah mengedit file, jalankan perintah validasi sintaks:
```bash
C:\xampp\php\php.exe -l application/modules/{nama_modul}/controllers/History.php
C:\xampp\php\php.exe -l application/modules/{nama_modul}/views/history.php
```
Pastikan menghasilkan: `No syntax errors detected`.

---

## 6. Daftar Inventaris Modul & Checklist Migrasi

| No | Modul | Path Controller | Status Migrasi | Catatan Endemik / Filter Khusus |
|:---:|:---|:---|:---:|:---|
| 1 | **penjualan** | `application/modules/penjualan/controllers/History.php` | ✅ **SELESAI (PILOT)** | Filter salesperson freelance (`transaksi.seller_id`) |
| 2 | **pembelian** | `application/modules/pembelian/controllers/History.php` | ⏳ Siap Migrasi | Pihak supplier, foreign currency / valas (`print_nvalas`) |
| 3 | **distribusifg** | `application/modules/distribusifg/controllers/History.php` | ⏳ Siap Migrasi | Filter gudang asal, surat jalan & ekspedisi |
| 4 | **penerimaan** | `application/modules/penerimaan/controllers/History.php` | ⏳ Siap Migrasi | Step penerimaan gudang, nomor PO |
| 5 | **invoicing** | `application/modules/invoicing/controllers/History.php` | ⏳ Siap Migrasi | Status faktur pajak, pelunasan |
| 6 | **kas** | `application/modules/kas/controllers/History.php` | ⏳ Siap Migrasi | Kolom kasir, penagihan, penerima/penyerah dana |
| 7 | **pembayaran** | `application/modules/pembayaran/controllers/History.php` | ⏳ Siap Migrasi | Aliran kas keluar / pelunasan hutang |
| 8 | **biaya** | `application/modules/biaya/controllers/History.php` | ⏳ Siap Migrasi | Akun beban & pajak supplier |
| 9 | **inputstok** | `application/modules/inputstok/controllers/History.php` | ⏳ Siap Migrasi | Penyesuaian stok manual / opname |
| 10 | **pindahgudang** | `application/modules/pindahgudang/controllers/History.php` | ⏳ Siap Migrasi | Mutasi antar-cabang/gudang |
| 11 | **produksi** | `application/modules/produksi/controllers/History.php` | ⏳ Siap Migrasi | SPK & hasil kerja mesin/operator |
| 12 | **returpenjualan**| `application/modules/returpenjualan/controllers/History.php`| ⏳ Siap Migrasi | Penerimaan kembali produk retur |

---

## 7. Jebakan Umum (Common Pitfalls & Gotchas) untuk AI Agent

1. **JANGAN lupa `require_once APPPATH . 'libraries/DatatablesHistory.php';`:**
   CI3 Wiredesignz HMVC tidak selalu me-load library pihak ketiga secara otomatis jika dipanggil lewat magic getter `$this->datatableshistory`. Selalu sertakan pengecekan `class_exists`.
2. **JANGAN set `$data['mode'] = 'showData'` di dalam `viewHistory()`:**
   Jika `mode` bernilai `showData`, template `history.html` tidak akan di-render dan halaman akan kehilangan navigasi, menu samping, serta header! Nilai `mode` pada `viewHistory()` harus `'viewHistory'`.
3. **Waspada Line Endings Windows (`\r\n` vs `\n`):**
   Saat melakukan replace pada view `history.php` yang berukuran besar (>3000 baris), jangan mengandalkan potongan teks pendek. Gunakan string unik atau skrip scratch berorientasi string pasti.
4. **Kompatibilitas PHP 5.6 Wajib Dijaga:**
   - Dilarang keras memakai operator `??` (gunakan `isset($x) ? $x : $default`).
   - Dilarang memakai short array `[]` (gunakan `array()`).
   - Dilarang anonymous classes atau arrow function `fn()`.
