# Dokumentasi: Controller `Items.php`

> Dibuat: 2026-07-18  
> File: [`app/Controllers/Items.php`](file:///w:/san_ibb_2jul/app/Controllers/Items.php)  
> Halaman: **Produk dijual** — `charly.mayagrahakencana.com/items/index/items`

---

## 1. Gambaran Umum

`Items` adalah controller **generik berbasis model dinamis** yang mengelola berbagai jenis data master CRM yang ditampilkan dalam format tabel (DataTable). Satu controller ini menangani halaman **Produk dijual**, **Satuan**, **Kategori konsumen**, **Sub bidang konsumen**, **Kategori item**, **Sub preferensi produk**, **Sumber**, **Sumber Prospek**, **Status prospek**, dan **Subsidiary** — semua bergantung pada **URL segment ke-3** untuk menentukan model mana yang digunakan.

```
URL Pattern:  /items/[method]/[model_name]
Contoh:       /items/index/items       → menggunakan Items_model
              /items/index/satuans     → menggunakan Satuans_model
              /items/modal_form/items  → modal form untuk Items_model
```

---

## 2. Mekanisme Model Dinamis (Relative Model)

> [!IMPORTANT]
> Ini adalah fitur inti controller ini. Satu controller melayani **banyak halaman** hanya dengan mengganti URL segment ke-3.

### Cara Kerja

Di dalam `__construct()` ([baris 17–43](file:///w:/san_ibb_2jul/app/Controllers/Items.php#L17-L43)):

```php
// Ambil nama model dari URL segment ke-3
$this->model = $model = url_segment(3);        // contoh: "items", "satuans"
$className = ucwords($model);                   // contoh: "Items", "Satuans"

// Bentuk nama class model dengan namespace penuh
$model_name = "App\\Models\\" . $className . "_model";

// Cek apakah model tersebut ada
if (class_exists($model_name)) {
    $this->Items_model = model($model_name);    // load model spesifik
} else {
    $this->Items_model = model("Items_model");  // fallback ke Items_model
}
```

### Contoh Pemetaan URL → Model

| URL Segment 3 | Class yang Dicoba | Fallback |
|---|---|---|
| `items` | `App\Models\Items_model` | `Items_model` |
| `satuans` | `App\Models\Satuans_model` | `Items_model` |
| `item_categories` | `App\Models\Item_categories_model` | `Items_model` |
| `clients` | `App\Models\Clients_model` | `Items_model` |

### Kontrak yang Harus Dipenuhi Model

Agar sebuah model dapat berfungsi penuh di controller ini, model harus mengimplementasikan:

| Method/Property | Keterangan |
|---|---|
| `getFields()` | Mengembalikan array definisi kolom yang akan ditampilkan di tabel dan form |
| `get_details($options)` | Query utama untuk list data |
| `get_one($id)` | Mengambil satu record |
| `ci_save($data, $id?)` | Menyimpan data (insert/update) |
| `delete($id, $undo?)` | Menghapus atau restore data |
| `getSyncroConfig()` *(opsional)* | Konfigurasi sinkronisasi API jika ada |
| `get_max_counter_value()` *(opsional)* | Untuk counter auto-increment |
| `upload_file_allowed` *(property)* | `true` jika model mengizinkan upload file |

---

## 3. Struktur Definisi Field (`getFields()`)

Controller membaca definisi field dari model melalui `getFields()`. Struktur yang didukung:

```php
[
  'nama_kolom' => [
    'form'          => true,              // tampil di form modal & tabel
    'column'        => 'nama_db',         // nama kolom di database (jika beda)
    'form_key'      => 'nama_post',       // nama POST key (jika beda)
    'fx'            => fn($v) => ...,     // fungsi transformasi nilai untuk tabel
    'form_fx'       => 'to_decimal_format', // fungsi transformasi saat save
    'links'         => [                  // jadikan link/anchor
      'url'  => 'items/view',
      'fx'   => fn(...) => ...
    ],
    'formates'      => [                  // format khusus (misal: gambar)
      'decoding' => fn($v) => unserialize($v),
      'image'    => true
    ],
    'optional'      => 'nama_method',     // method di controller untuk isi dropdown
    'form_datas'    => 'varname_view',    // nama variabel yang dikirim ke view
    'form_data_model' => 'NamaModel',     // model yang dipakai untuk dropdown
    'form_data_view' => 'kolom_label',    // kolom yang dijadikan label dropdown
    'manages'       => ['label_key' => '...'], // untuk fitur manage parent/folder
  ]
]
```

---

## 4. Daftar Method Public

### 4.1 `index()` — Halaman List Data

**URL:** `GET /items/index/[model]`

Menampilkan halaman daftar data dalam format tabel. Method ini:

1. Validasi hak akses (`validate_access_to_items`)
2. Cek apakah model memiliki `getSyncroConfig()` → tampilkan tombol **Sinkron data** jika ada
3. Baca field definisi via `_get_fields()`
4. Deteksi kolom `optional` → panggil method controller terkait untuk mengisi dropdown filter
5. Deteksi kolom `manages` → aktifkan fitur kelola parent (tombol **Kelola kategori**, **Kelola satuan**, dll.)
6. Render view `items/index`

**View data yang dikirim:**

| Key | Keterangan |
|---|---|
| `$model` | Nama model aktif (dari URL segment 3) |
| `$field_to_view` | Array definisi kolom |
| `$can_syncro_datas` | Boolean, tombol sinkron muncul atau tidak |
| `$can_edit_datas` | Boolean, hak akses edit |
| `$api_base_url` | Base URL Data Center API |
| `$can_manage_optional` | Boolean, ada kolom yang bisa di-manage |
| `$manages` | Array info model yang bisa dikelola |

---

### 4.2 `list_data()` — Data untuk DataTable (AJAX)

**URL:** `POST /items/list_data/[model]`

Dipanggil oleh DataTable untuk mengambil data via AJAX. Filter yang didukung:

| POST Parameter | Keterangan |
|---|---|
| `category_id` | Filter berdasarkan kategori |
| `jenis` | Filter berdasarkan jenis (`item`, `paket`, `item_rakitan`, dll.) |
| `folders_id` | Filter berdasarkan sub kategori/folder |
| `search` | Pencarian teks bebas |

---

### 4.3 `modal_form()` — Form Tambah/Edit (AJAX)

**URL:** `POST /items/modal_form/[model]`

Menampilkan form modal untuk menambah atau mengedit data. Fitur:
- Membaca definisi field dari `_get_fields_modal()`
- Jika request memiliki GET param `?x=...`, aktifkan mode **managable** (kelola parent)
- Jika model memiliki `upload_file_allowed = true`, aktifkan form upload
- Jika model adalah `domain`, aktifkan upload template khusus

---

### 4.4 `save()` — Simpan Data (AJAX)

**URL:** `POST /items/save/[model]`

Menyimpan data (insert/update) secara dinamis berdasarkan field yang didefinisikan di `getFields()`. Alur:

1. Baca semua field dari `_get_fields_modal()`
2. Iterasi field, ambil nilai POST, terapkan fungsi transformasi (`form_fx`)
3. Jika model punya `get_max_counter_value()`, set kolom `counter` otomatis
4. Jika ada file yang diupload, proses ke direktori permanen
5. Simpan via `Items_model->ci_save()`

---

### 4.5 `delete()` — Hapus/Restore Data (AJAX)

**URL:** `POST /items/delete/[model]`

Mendukung **soft delete** dan **undo**. Jika POST `undo` dikirim, record dipulihkan.

---

### 4.6 `syncro_data()` — Sinkronisasi dari API Eksternal

**URL:** `POST /items/syncro_data/[model]`

Menarik data dari API eksternal (Data Center) lalu menyimpannya ke database lokal. Hanya bisa dijalankan oleh user yang memiliki hak `can_syncro_datas`.

Mendukung dua model:

| Model | Handler Internal | Keterangan |
|---|---|---|
| `items` | `_sync_items_data()` | Sinkron produk + varian |
| `clients` | `_sync_clients_data()` | Sinkron customer (batch, 150/call) |

---

### 4.7 `variant_detail_modal()` — Modal Detail Varian Produk

**URL:** `POST /items/variant_detail_modal/[model]`

Menampilkan varian-varian dari sebuah produk. Mendukung tabel varian opsional:
- `var_product_variants`
- `var_product_variant_values`
- `var_attributes` / `var_attribute_values`
- `var_size_scale_values` *(opsional)*

---

### 4.8 `view()` — Detail Item (Modal)

Menampilkan detail satu item dalam modal, dengan data tambahan `client_info`.

---

### 4.9 `save_files_sort()` — Urutkan File Lampiran

Menyimpan urutan file lampiran yang sudah di-drag-drop oleh user.

---

### 4.10 `download_sample_excel_file()` — Unduh Template Excel

Mengunduh file `import-items-sample.xlsx` sebagai template untuk import massal.

---

## 5. Method Private Penting

### `_get_fields()` dan `_get_fields_modal()`

- `_get_fields()` mengambil semua field yang punya key `form` dari `getFields()` model
- `_get_fields_modal()` subset dari `_get_fields()`, hanya yang `form === true`

### `_make_item_row($data)` — Render Baris Tabel

Mengubah satu object data menjadi array HTML untuk DataTable. Logika rendering kolom:

| Kondisi | Hasil |
|---|---|
| Ada `links` | Dirender sebagai `modal_anchor` |
| Ada `formates` + `image` | Dirender sebagai tag `<img>` |
| Kolom `variant_count` | Dirender sebagai pill badge, bisa diklik jika ada varian |
| Default | Nilai ditampilkan apa adanya |

Kolom aksi edit/delete selalu ditambahkan di akhir setiap baris.

### `_sync_items_data($datas)` — Proses Sinkron Produk

Memproses response dari API Data Center:
- **Insert** jika produk belum ada (berdasarkan `id`)
- **Update** jika produk sudah ada
- Support sinkronisasi data varian (`data_varian` di payload) melalui `sync_variant_data_blocks()`
- Menggunakan **transaction database** (`transBegin/transCommit/transRollback`)

### `_sync_clients_data($datas)` — Proses Sinkron Customer

- Support **batch processing** via GET param `batch_start` dan `batch_size` (default 150/batch)
- Normalisasi dan resolving alamat wilayah (provinsi, kabupaten, kecamatan, kelurahan, kodepos) via `_resolve_postal_location_ids()`
- Pencocokan data dilakukan berdasarkan `dc_id` (jika kolom ada) atau `company_name`

### `_resolve_postal_location_ids()` — Resolving Wilayah

Mencari ID wilayah di tabel `rise_postal_codes` dengan strategi bertingkat (dari paling spesifik ke paling umum):
1. Kodepos + Provinsi
2. Provinsi + Kabupaten + Kecamatan + Kelurahan
3. Provinsi + Kabupaten + Kecamatan
4. Provinsi + Kabupaten
5. Hanya Provinsi

---

## 6. Dropdown Helper Methods

| Method | Keterangan |
|---|---|
| `_get_jenis_dropdown()` | Dropdown jenis produk: `item`, `item_rakitan`, `item_komposit`, `paket` |
| `_get_folders_dropdown()` | Dropdown sub kategori (dari kolom `folders_nama` di tabel items) |
| `_get_categories_dropdown()` | Dropdown kategori dari `Item_categories_model` |

Method ini dipanggil secara **dinamis** dari `index()` jika ada field dengan key `optional` yang cocok dengan nama method ini.

---

## 7. Kontrol Akses

Kontrol akses ditetapkan di `validate_access_to_items()`:

| Kondisi | Akses |
|---|---|
| `is_admin` | Diizinkan |
| `access_invoice === 'all'` | Diizinkan |
| `access_estimate === 'all'` | Diizinkan |
| Punya permission `data_mgk` | Diizinkan |
| Modul `invoice`, `estimate`, atau `data_mgk` tidak aktif | Redirect ke `forbidden` |

---

## 8. Import Excel

Controller menggunakan trait `Excel_import` dengan header kolom:

| Kolom | Wajib |
|---|---|
| `title` | Ya |
| `category` | Ya |
| `rate` | Ya |
| `description` | - |
| `unit_type` | - |
| `show_in_client_portal` | - |

---

## 9. Diagram Alur Utama

```mermaid
sequenceDiagram
    actor User
    participant Router as CodeIgniter Router
    participant Controller as Items Controller
    participant Model as Model Dinamis
    participant DB as Database

    User->>Router: GET /items/index/items
    Router->>Controller: __construct() ambil url_segment(3) = "items"
    Controller->>Controller: class_exists("App\Models\Items_model") = true
    Controller->>Model: model("App\Models\Items_model")
    Controller->>Controller: index()
    Controller->>Model: getFields()
    Controller->>User: Render view items/index

    User->>Controller: POST /items/list_data/items via AJAX
    Controller->>Model: get_details($options)
    Model->>DB: SQL Query
    DB-->>Model: Result
    Controller->>User: JSON dengan array data tabel
```

---

## 10. Catatan Pengembangan

> [!NOTE]
> Method `test()`, `testbaca()`, `cek()`, dan `doResetCrmData()` adalah method utilitas/debugging yang ada di controller ini. Pastikan akses ke method tersebut terlindungi atau dihapus sebelum production.

> [!TIP]
> Untuk menambahkan halaman baru di bawah controller `Items`, cukup:
> 1. Buat model baru `App\Models\NamaModel_model` yang mengimplementasikan `getFields()`, `get_details()`, `get_one()`, `ci_save()`, dan `delete()`
> 2. Akses via URL `/items/index/nama_model`
> 3. Tidak perlu mengubah controller sama sekali

> [!WARNING]
> Nama model diambil langsung dari URL segment ke-3 menggunakan `ucwords()`. Pastikan nama URL segment **sesuai persis** dengan nama class model (case-insensitive untuk ucwords, tapi namespace-sensitive untuk `class_exists`).
