# Blueprint: Fitur "Buat Baru" pada Penawaran (Estimates)
**Referensi:** CRM Update v14 — Poin 1  
**Tanggal Diskusi:** 2026-07-02  
**Status:** 🔲 Siap Implementasi (kecuali bagian bertanda 🔲 Dibahas Terpisah)

---

## 1. Latar Belakang

Sales membutuhkan kemampuan untuk menambahkan produk secara ad-hoc ke dalam penawaran, tanpa harus merujuk ke produk yang sudah terdaftar di Data Center (`items`). Produk ini bersifat sementara dan eksklusif untuk penawaran tersebut.

Di akhir siklus, produk ad-hoc ini **wajib di-mapping** ke produk riil di Data Center sebelum penawaran dapat dikonversi menjadi Sales Order.

---

## 2. Keputusan Desain

| Aspek | Keputusan |
|-------|-----------|
| **Penyimpanan** | Tabel baru `estimate_custom_items`, terpisah dari tabel `items` |
| **Eksklusivitas** | Terikat ke 1 `estimate_id`. Tidak reusable antar penawaran |
| **Field input** | `item_name`, `unit`, `rate`, `description` — semua **tidak mandatory** |
| **Mapping produk riil** | Kolom `mapped_item_id` (FK ke `items`), `mapped_by`, `mapped_at` |
| **Blokir Sales Order** | Jika ada baris dengan `mapped_item_id IS NULL` → konversi ke SO ditolak |
| **Hak akses mapping** | Mengikuti hak akses penawaran yang berlaku saat ini (tidak ada perubahan role) |
| **Modifikasi `estimate_items`** | ❌ Tidak diperlukan — JOIN via `estimate_id` sudah cukup |
| **Urutan item** | Diatur via kolom `sort_order` di `estimate_custom_items` |
| **Audit trail** | Soft delete + full timestamp siapa yang buat/edit/hapus |
| **Alur ERP saat Won** | 🔲 Dibahas terpisah (lebih kompleks) |

---

## 3. Skema Tabel Baru: `estimate_custom_items`

```sql
CREATE TABLE `estimate_custom_items` (
  `id`             INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `estimate_id`    INT(11) UNSIGNED NOT NULL COMMENT 'FK ke tabel estimates',

  -- Data produk custom (input bebas sales)
  `item_name`      VARCHAR(255)     NULL DEFAULT NULL COMMENT 'Nama produk sementara',
  `unit`           VARCHAR(50)      NULL DEFAULT NULL COMMENT 'Satuan produk',
  `rate`           DECIMAL(15,2)    NULL DEFAULT NULL COMMENT 'Harga satuan',
  `description`    TEXT             NULL DEFAULT NULL COMMENT 'Keterangan tambahan',

  -- Relasi ke produk riil (diisi saat fase mapping)
  `mapped_item_id` INT(11) UNSIGNED NULL DEFAULT NULL COMMENT 'FK ke tabel items, NULL = belum di-mapping',
  `mapped_by`      INT(11) UNSIGNED NULL DEFAULT NULL COMMENT 'user_id yang melakukan mapping',
  `mapped_at`      DATETIME         NULL DEFAULT NULL COMMENT 'Waktu mapping dilakukan',

  -- Audit trail
  `created_by`     INT(11) UNSIGNED NULL DEFAULT NULL COMMENT 'user_id yang membuat',
  `created_at`     DATETIME         NULL DEFAULT NULL,
  `updated_by`     INT(11) UNSIGNED NULL DEFAULT NULL COMMENT 'user_id terakhir yang mengedit',
  `updated_at`     DATETIME         NULL DEFAULT NULL,
  `deleted_by`     INT(11) UNSIGNED NULL DEFAULT NULL COMMENT 'user_id yang menghapus (soft delete)',
  `deleted_at`     DATETIME         NULL DEFAULT NULL,
  `is_deleted`     TINYINT(1)       NOT NULL DEFAULT 0 COMMENT '0=aktif, 1=dihapus',

  -- Urutan tampil dalam penawaran
  `sort_order`     INT(11)          NOT NULL DEFAULT 0 COMMENT 'Urutan tampil item dalam penawaran',

  PRIMARY KEY (`id`),
  KEY `idx_estimate_id` (`estimate_id`),
  KEY `idx_mapped_item_id` (`mapped_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Produk custom/ad-hoc yang dibuat sales per penawaran';
```

---

## 4. Pendekatan Rendering: UNION Query

Tabel `estimate_items` **tidak dimodifikasi**. Saat merender daftar item pada sebuah penawaran, sistem menggunakan UNION dari dua tabel, diurutkan berdasarkan `sort_order`:

```sql
SELECT
  'regular'  AS item_type,
  ei.id,
  ei.item_id,
  ei.item_name,
  ei.rate,
  ei.quantity,
  ei.sort_order
FROM estimate_items ei
WHERE ei.estimate_id = <estimate_id>

UNION ALL

SELECT
  'custom'   AS item_type,
  eci.id,
  NULL       AS item_id,
  eci.item_name,
  eci.rate,
  NULL       AS quantity,
  eci.sort_order
FROM estimate_custom_items eci
WHERE eci.estimate_id = <estimate_id>
  AND eci.is_deleted = 0

ORDER BY sort_order ASC
```

> **Catatan:** Kolom `item_type` digunakan oleh layer View/Controller untuk membedakan rendering item reguler vs item custom (misal: menampilkan badge "Custom" atau tombol "Mapping ke Produk Riil").

---

## 5. Lifecycle Status Item Custom

```
[DRAFT / INPUT SALES]
        |
  is_custom = 1
  custom_item_id = <id>
  mapped_item_id = NULL         <- status: UNRESOLVED
        |
  (user dengan hak akses penawaran melakukan mapping)
        |
  mapped_item_id = <id dari items>
  mapped_by = <user_id>
  mapped_at = <timestamp>       <- status: RESOLVED
        |
  Konversi ke Sales Order diizinkan
```

---

## 6. Logika Blokir Konversi ke Sales Order

Sebelum proses konversi dari Estimate ke Sales Order dijalankan, sistem wajib melakukan pengecekan:

```sql
SELECT COUNT(*) FROM estimate_custom_items
WHERE estimate_id = <estimate_id>
  AND mapped_item_id IS NULL
  AND is_deleted = 0
```

Jika hasilnya `> 0` maka **batalkan konversi** dan tampilkan notifikasi kepada user berisi daftar item yang masih belum di-mapping.

---

## 7. Komponen Codebase yang Terdampak

| File | Perubahan |
|------|-----------|
| `app/Views/estimates/item_modal_form.php` | Tambah tombol/opsi "Buat Baru" dan form input ad-hoc |
| `app/Controllers/Estimates.php` fungsi `save_item` | Handle penyimpanan ke `estimate_custom_items` + set flag `is_custom=1` di `estimate_items` |
| `app/Controllers/Estimates.php` fungsi konversi ke SO | Tambah validasi cek `mapped_item_id IS NULL` sebelum konversi |
| `app/Models/Estimate_items_model.php` (atau sejenis) | Query untuk CRUD `estimate_custom_items` |
| *(View mapping)* | UI untuk melakukan mapping item custom ke produk riil (halaman baru atau modal) |

---

## 8. Yang Belum Diputuskan (Backlog)

| Item | Keterangan |
|------|------------|
| 🔲 **Alur ERP saat Won** | Bagaimana data item custom terbaca/dinotifikasi ke ERP saat penawaran berstatus Won. Akan dibahas terpisah. |

---

*Dokumen ini merupakan catatan hasil diskusi desain. Implementasi belum dimulai.*
