# Modul Data Master

Mencakup: Cabang (Branch), Kategori Produk, Satuan (Unit) & Konversi Satuan, Modifier, Manajemen Meja (Table), Pelanggan (Customer).

Semua sub-modul ini mengikuti pola CRUD yang hampir identik (lihat [00-overview.md](00-overview.md)): List (client-side pagination) → Form create/edit dengan `useQuery` untuk detail + `useMutation` untuk simpan → `invalidateQueries` + toast + redirect balik ke list saat sukses.

---

## 1. Cabang (Branch)

Service: [branchService.js](../../src/services/branchService.js) · Form: [BranchForm.jsx](../../src/pages/master/BranchForm.jsx)

**Field form (lengkap):** `name*`, `phone*`, `pos_code*`, `description`, `manager_name*`, `manager_phone*`, `address`, `province/city/district/village` (cascading dropdown dari `regionService`), `email*`, `website`, `instagram`, `x_handle`, `facebook`, `tiktok`, `rounding_type` (round_up/round_down/round_near/none), `rounding_value`, `void_pin` (4–8 karakter, khusus cabang ini — kalau kosong pakai PIN tenant), logo (file upload).

> Tanda `*` di UI = wajib secara visual, tapi **validasi JS yang benar-benar dijalankan sebelum submit cuma cek `form.name`** — field lain yang ditandai wajib di label tidak benar-benar divalidasi di FE (potensi bug kecil: user bisa submit tanpa phone/pos_code/manager_name/email meski label bertanda *). Backend mungkin reject via 422 kalau field itu required di Laravel — toast pertama dari `Object.values(errors).flat()[0]` akan tampil.

**Cascading region:** `province → city → district → village` — tiap level fetch dari `GET /v1/regions/{provinces|cities/{id}|districts/{id}|villages/{id}}`, `staleTime: Infinity` (di-cache permanen per session). Pilih provinsi mereset city/district/village di bawahnya.

**Flow submit:**
- Kalau ada file logo baru dipilih → kirim sebagai `multipart/form-data` (FormData). Untuk edit, pakai trik `_method=PUT` di body karena PHP tidak bisa parse multipart lewat method PUT asli — request sebenarnya dikirim via `POST`.
- Kalau tidak ada logo baru → kirim JSON biasa via `PUT` (edit) / `POST` (create).
- Field kosong (selain `name`, `rounding_type`, `rounding_value`) dikirim sebagai `null`, bukan string kosong.
- `website` otomatis di-prefix `https://` kalau user tidak mengetik protokol.
- `void_pin` hanya dikirim kalau diisi (tidak overwrite ke kosong saat edit jika dibiarkan blank).

**Endpoint:**
| Aksi | Method | Endpoint |
|---|---|---|
| List | GET | `/v1/branches` |
| Detail | GET | `/v1/branches/{id}` |
| Create | POST | `/v1/branches` |
| Update | PUT (atau POST + `_method=PUT` kalau multipart) | `/v1/branches/{id}` |
| Delete | DELETE | `/v1/branches/{id}` |
| Export Excel/PDF | GET | `/v1/branches/export/excel`, `/v1/branches/export/pdf` (blob) |
| Template import | GET | `/v1/branches/import/template` (blob) |
| Import Excel | POST | `/v1/branches/import` (multipart, field `file`) |

**Catatan:** Sesuai memori sebelumnya, kemungkinan backend hanya benar-benar menyimpan subset field inti (`name, address, phone, is_active`) — field lain (social media, rounding, void_pin, logo) mungkin sudah/belum semua tersimpan persisten di backend. **Perlu verifikasi langsung ke backend** kalau field-field tambahan ini krusial untuk keputusan bisnis.

---

## 2. Kategori Produk (Category)

Service: [categoryService.js](../../src/services/categoryService.js) · Form: [CategoryForm.jsx](../../src/pages/master/CategoryForm.jsx)

**Field:** `name*` (satu-satunya yang divalidasi: `!form.name.trim()` → toast "Nama kategori wajib diisi"), `description` (textarea, max 500 char dihitung di UI tapi tidak di-`maxLength` enforce ketat selain atribut HTML), `branch_ids[]` (multi-select cabang — kalau kosong dianggap "Semua Cabang / default" oleh UI, tapi tetap dikirim sebagai array kosong `[]` ke backend).

**Endpoint:** `GET/POST/PUT/DELETE /v1/categories` (+ `/{id}`), plus export Excel/PDF dan import Excel (pola sama dengan Branch).
**Body create/update:** `{ name, description, branch_ids: [] }`

---

## 3. Satuan (Unit) & Konversi Satuan

Service: [unitService.js](../../src/services/unitService.js) · Forms: [UnitForm.jsx](../../src/pages/master/UnitForm.jsx), [UnitConversionForm.jsx](../../src/pages/master/UnitConversionForm.jsx)

### Unit
**Field:** `name*`, `symbol*` (mis. "kg", "pcs"), `description` (ada di state tapi textarea-nya **dikomentari/disembunyikan** di UI — field ini tidak bisa diisi user saat ini meski ada di payload).
**Validasi:** hanya `!form.name.trim()`.
**Endpoint:** `GET/POST/PUT/DELETE /v1/units` (+ `/{id}`). Body: `{ name, symbol, description }`.

### Konversi Satuan (Unit Conversion)
Dipakai untuk konversi antar satuan (misal 1 kg = 1000 gram) — relevan untuk modul Inventory.

**Form unik:** mode **create** mendukung **multi-row** (tambah beberapa konversi sekaligus dalam 1 tabel, tombol "Tambah" nambah baris), tapi setiap baris dikirim sebagai **request `POST` terpisah** (loop `for...of` dengan `storeMutation.mutateAsync` berurutan — bukan batch endpoint). Mode **edit** cuma 1 baris (`PUT`).

**Field per baris:** `from_unit_id*`, `to_unit_id*`, `multiplier*` (number). Validasi manual: cek tiap baris punya 3 field ini sebelum submit.

**Endpoint:** `GET /v1/unit-conversions` (list semua, dipakai juga untuk cari detail by id di FE karena tidak ada endpoint show terpisah), `POST /v1/unit-conversions`, `PUT /v1/unit-conversions/{id}`, `DELETE /v1/unit-conversions/{id}`.

**Catatan performa:** kalau user tambah 5 baris sekaligus di create mode → 5 request HTTP sequential, bukan paralel/batch. Untuk jumlah besar bisa lambat.

---

## 4. Modifier

Service: [modifierService.js](../../src/services/modifierService.js) · Form: [ModifierForm.jsx](../../src/pages/master/ModifierForm.jsx)

Modifier = pilihan tambahan produk (misal "Level Pedas", "Ukuran") dengan opsi-opsi (`options`) yang masing-masing punya harga tambahan.

**Field:**
- `name*`, `description` (textarea, max 500)
- `select_type`: `single` (wajib pilih 1) atau `multiple` (boleh lebih dari 1) — radio button
- `options[]`: tiap opsi punya `name`, `price`. **Hanya opsi yang `name`-nya tidak kosong yang ikut dikirim** (`options.filter(o => o.name.trim())`).
- `product_ids[]`: multi-select produk mana yang pakai modifier ini

**Catatan penting (komentar di kode):** kolom "Ambil dari Bahan Baku" per opsi (`use_ingredient` + `ingredients: [{raw_material_id, qty}]`) — fitur potong stok bahan baku otomatis saat opsi modifier dipilih — **sengaja disembunyikan dari UI** karena belum ada implementasi backend untuk menyimpan/memakai data ini. State-nya masih ada di kode (`use_ingredient`, `ingredients`) tapi **tidak pernah dikirim** ke payload submit (`saveMutation.mutate` hanya kirim `name` & `price` per opsi). Jangan dikira fitur ini aktif.

**Validasi:** hanya `!form.name.trim()`.

**Endpoint:**
| Aksi | Method | Endpoint |
|---|---|---|
| List/Detail/CRUD modifier | GET/POST/PUT/DELETE | `/v1/modifiers` (+`/{id}`) |
| CRUD options | POST/PUT/DELETE | `/v1/modifiers/{modifierId}/options` (+`/{optionId}`) — **tersedia di service tapi tidak dipanggil dari ModifierForm** (form mengirim options langsung dalam payload modifier, bukan lewat endpoint options terpisah) |

**Body create/update:** `{ name, description, select_type, product_ids: [], options: [{ name, price }] }`

---

## 5. Manajemen Meja (Table)

Service: [tableService.js](../../src/services/tableService.js) · Form: [TableForm.jsx](../../src/pages/master/TableForm.jsx)

**Field:** `code*` (mis. "Meja 1"), `capacity*` (dropdown pilihan tetap: 2/4/6/8/10/12 orang), `description` (ada di state tapi UI-nya dikomentari/disembunyikan), `is_active` (toggle "Tersedia"/"Tidak Tersedia"), `branch_id*` (dropdown cabang).

**Validasi manual sebelum submit (urutan):** `code` kosong → toast, lalu `capacity` kosong → toast, lalu `branch_id` kosong → toast.

**Fitur khusus — QR Code meja:** `GET /v1/tables/{id}/qr` mengembalikan **blob** (gambar QR untuk dipasang di meja, dipakai pelanggan untuk order via QR — terhubung ke aplikasi `web_qr_order_frontend`). Dummy mode generate SVG placeholder sederhana.

**Endpoint:** `GET/POST/PUT/DELETE /v1/tables` (+`/{id}`), `GET /v1/tables/{id}/qr` (blob), plus export Excel/PDF dan import Excel.
**Body create/update:** `{ code, capacity, description, is_active, branch_id }`

---

## 6. Pelanggan (Customer)

Service: [customerService.js](../../src/services/customerService.js) · Form: [CustomerForm.jsx](../../src/pages/master/CustomerForm.jsx)

**Field aktif di UI:** `name*`, `phone`, `email`. (`address` & `notes` ada di state form tapi textarea-nya **dikomentari/disembunyikan** — tidak bisa diisi user, meski tetap dikirim sebagai string kosong `''` ke payload).

**Validasi:** hanya `!form.name.trim()`.

**Catatan:** ini satu-satunya service Master Data yang **tidak punya mode dummy** (`customerService.js` tidak import `USE_DUMMY`, selalu langsung panggil API asli) — pastikan backend selalu hidup saat development kalau kerja di modul ini, walau `VITE_USE_DUMMY=true`.

**Endpoint:** `GET/POST/PUT/DELETE /v1/customers` (+`/{id}`, support query `params` di index untuk search/filter kalau backend mendukung), plus export Excel/PDF. Tidak ada fitur import untuk Customer.

---

## Region service (pendukung Branch & lainnya)

`regionService.js` — read-only, dipakai untuk dropdown cascading provinsi/kota/kecamatan/kelurahan (sumber data wilayah Indonesia di backend, bukan API eksternal):

| Level | Endpoint | Param |
|---|---|---|
| Provinsi | `GET /v1/regions/provinces` | — |
| Kota/Kabupaten | `GET /v1/regions/cities/{provinceId}` | id provinsi |
| Kecamatan | `GET /v1/regions/districts/{cityId}` | id kota |
| Kelurahan/Desa | `GET /v1/regions/villages/{districtId}` | id kecamatan |
