# Kitchen Display System — Source of Truth

> **Scope:** Dokumen ini adalah referensi teknis definitif untuk Kitchen Display System (KDS). Mencakup arsitektur, alur data, file-file terkait, API endpoints, socket events, dan panduan modifikasi.

---

## Daftar Isi

1. [Overview & Arsitektur](#1-overview--arsitektur)
2. [File yang Terlibat](#2-file-yang-terlibat)
3. [Cara Akses & Role](#3-cara-akses--role)
4. [User Filtering (j_section & barang_ids)](#4-user-filtering-j_section--barang_ids)
5. [Alur Data End-to-End](#5-alur-data-end-to-end)
6. [Controller Actions (API Endpoints)](#6-controller-actions-api-endpoints)
7. [Socket.IO Events](#7-socketio-events)
8. [Struktur HTML Panel (x_panel)](#8-struktur-html-panel-x_panel)
9. [JavaScript Functions](#9-javascript-functions)
10. [Layout: main_kitchen.php](#10-layout-main_kitchenphp)
11. [Audio System](#11-audio-system)
12. [WIP Modal](#12-wip-modal)
13. [CSS Class Reference](#13-css-class-reference)

---

## 1. Overview & Arsitektur

Kitchen Display System adalah halaman real-time yang menampilkan order dari kasir kepada staff dapur. Order ditampilkan dalam tiga kolom yang merepresentasikan status masak:

```
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  NEW ORDER   │  │  ON PROCESS  │  │   FINISHED   │
│              │  │              │  │              │
│  [Card #A01] │  │  [Card #A01] │  │  [Card #A01] │
│  [Card #B03] │  │              │  │  [Card #C02] │
└──────────────┘  └──────────────┘  └──────────────┘
       │                 │                 │
   (Process btn)     (Done btn)       (PickUp btn)
       ▼                 ▼                 ▼
   status_masak=1    status_masak=2    isTake=-2
```

**Aliran status item:**

```
New Order (status_masak = null/0)
    → [Staff klik Process] → On Process (status_masak = 1)
    → [Staff klik Done]    → Finished (status_masak = 2)
    → [Staff klik PickUp]  → Dihapus dari layar (isTake = -2)
```

**Teknologi real-time:** Socket.IO — server POS mengirim event ke semua klien kitchen di cabang yang sama saat ada order baru atau perubahan status.

---

## 2. File yang Terlibat

| File | Peran |
|------|-------|
| `backend/views/layouts/main_kitchen.php` | Layout wrapper — toolbar atas, subToolbar, audio unlock, fullscreen, clock |
| `backend/views/penjualan/kitchen_display.php` | View konten — kolom New/Process/Finish, modal History, modal WIP |
| `backend/controllers/PenjualanController.php` | Semua action HTTP untuk kitchen (loaditem, neworder, updateitemstatus, dll.) |
| `backend/web/js/kitchen.screen.js` | Core JS — render panel, handle button clicks, WIP form |
| `backend/web/js/mainku.js` | Socket.IO event listeners — menerima `notif_pesanan_baru`, `finish_take`, dll. |
| `backend/assets/KitchenAsset.php` | Asset bundle — mendaftarkan Bootstrap, FA, animate.css, socket.io, JS files |

---

## 3. Cara Akses & Role

**URL:** `GET /penjualan/kitchen`

**Auto-redirect:** Jika user login dengan `jabatan = 'kitchen'`, `SiteController::actionIndex()` langsung redirect ke halaman ini tanpa melalui dashboard.

```php
// SiteController.php:299
if ($userx->jabatan == 'kitchen') {
    return $this->redirect(['penjualan/kitchen']);
}
```

**Role yang berlaku:**

| `jabatan` | Tampilan | Keterangan |
|-----------|----------|------------|
| `kitchen` | 3 kolom penuh | Bisa gerakkan order dari New → Process → Finish → PickUp |
| `checker` | Hanya kolom Finished | Kolom New dan On Process disembunyikan via class `hilang` |

Penentuan `isChecker` terjadi di controller:

```php
// PenjualanController::actionKitchen()
'isChecker' => $uAct->jabatan == User::JABATAN_CHECKER ? 'hilang' : '',
```

Class `hilang` (`display: none`) diinjeksi ke `col-md-4` untuk kolom New dan On Process.

---

## 4. User Filtering (j_section & barang_ids)

Setiap user kitchen bisa dikonfigurasi untuk hanya melihat order dari section atau menu tertentu. Ini memungkinkan satu dapur memiliki beberapa station dengan layar terpisah.

### Field pada tabel `user`

| Field | Tipe | Isi | Contoh |
|-------|------|-----|--------|
| `j_section` | JSON string | Array of `top_katagori.id` | `[1, 3]` |
| `barang_ids` | JSON string | Array of `barang.id` | `[12, 15, 20]` |

### Logika filter di `actionLoaditem()` dan `actionNeworder()`

```
Tampilkan item JIKA:
  (barang_id ada di user.barang_ids  OR  top_katagori.id ada di user.j_section)
  AND barang_id TIDAK terdaftar di user.barang_ids milik user lain
```

**Penting:** `barang_ids` bersifat eksklusif per user. Jika menu X terdaftar di User A, maka User B tidak akan melihat menu X meskipun User B punya section yang mencakup menu X. Ini mencegah duplikasi tampilan order antar station.

```php
// Kode di actionLoaditem() — cek eksklusivitas
if (in_array($model->barang_id, $barangIdsInOtherUser)) {
    continue; // skip, sudah di-handle user lain
}
```

### Inisialisasi filter di Controller

```php
// Di __construct() atau behavior PenjualanController
if ($user->j_section) {
    $this->_userSections = json_decode($user->j_section);
}
if ($user->barang_ids) {
    $this->_barangIds = json_decode($user->barang_ids);
}
```

### Tampilan filter di subToolbar (main_kitchen.php)

Section dan menu aktif ditampilkan di subToolbar biru di bawah toolbar utama. Klik subToolbar-text membuka modal detail filter.

```php
$sectionNames = TopKatagori::find()->select('nama')->where(['id' => $userSectionIds])->column();
$barangNames  = Barang::find()->select('nama')->where(['id' => $userBarangIds])->column();
```

---

## 5. Alur Data End-to-End

### 5.1 Initial Page Load

```
1. Browser buka /penjualan/kitchen
2. actionKitchen() render layout + view
3. JS: window.addEventListener('load') → loadItem() + setNotif()
4. loadItem() → POST /penjualan/loaditem
5. Server: query PenjualanItem dgn filter user, date=today, in_kitchen=1
6. Response: array of formatResponseKitchen($model)
7. JS: $.each(data) → addPanel(item, symbol) atau addRow(item, symbol)
8. togleTomorrow(true) — sembunyikan order besok secara default
```

### 5.2 Order Baru Masuk (Real-time)

```
1. Kasir buat order → server emit socket "notif_pesanan_baru{cabangId}"
2. mainku.js: socket.on("notif_pesanan_baru{cabangId}") → place_new_order_kitchen(id)
3. place_new_order_kitchen() → POST /penjualan/neworder {id: penjualan_id}
4. actionNeworder() → query PenjualanItem dgn filter, return array
5. JS: $.each(data) → addPanel / addRow untuk setiap item
6. animateCSS('fabell', 'swing') — bell animation
7. notif.currentTime = 0; notif.play() — putar notifikasi HANYA saat order baru
8. applyKitchenFilter() — re-terapkan filter search jika sedang aktif
```

### 5.3 Status Transition: New → On Process

```
1. Staff klik tombol [Process] pada panel
2. gotoNext(pjId, itemId, status=1)
3. $("#trItem{itemId}").remove() — hapus row dari panel New
4. POST /penjualan/updateitemstatus {idx: itemId, status: 1}
5. Server: update PenjualanItem.status_masak = 1
6. Response: formatResponseKitchen($model) — data item yang di-update
7. JS: addPanel(data, 'P') atau addRow(data, 'P') — tambah ke kolom Process
8. Jika panel New kosong → hapus panel New
9. countPanel() — update counter di header kolom
```

### 5.4 Status Transition: On Process → Finished

```
1. Staff klik tombol [Done] pada panel
2. gotoNext(pjId, itemId, status=2)
3. Hapus row dari panel Process
4. POST /penjualan/updateitemstatus {idx: itemId, status: 2}
5. JS: addPanel(data, 'F') atau addRow(data, 'F') — tambah ke kolom Finished
6. Jika semua item penjualan_id di kolom Process habis:
   a. Hapus panel Process
   b. showBtnPickup(pjId) → tampilkan tombol [PickUp] di panel Finished
   c. POST /penjualan/finish → server update Penjualan.status_masak = 'done'
   d. recall(itemId) → POST /penjualan/recall (notif ke cashier)
7. countPanel()
```

### 5.5 Pick Up (Finished → Selesai)

```
1. Staff klik tombol [PickUp]
2. gotoPickup(pjId)
3. animateCSS("pnlF{id}", 'bounceOutUp') — animasi keluar
4. POST /penjualan/take {idx: pjId}
5. Server: update Penjualan.isTake, emit socket "finish_take{cabangId}"
6. Response: {urutan: "A001"}
7. JS: update teks #lastPickup → "#A001"
8. Panel dihapus dari DOM
```

---

## 6. Controller Actions (API Endpoints)

Semua action berada di `backend/controllers/PenjualanController.php`.

| Action | Method | URL | Akses dari |
|--------|--------|-----|-----------|
| `actionKitchen` | GET | `/penjualan/kitchen` | Browser (halaman utama) |
| `actionLoaditem` | POST | `/penjualan/loaditem` | `kitchen.screen.js` — initial load |
| `actionNeworder` | POST | `/penjualan/neworder` | `mainku.js` — via socket event |
| `actionUpdateitemstatus` | POST | `/penjualan/updateitemstatus` | `kitchen.screen.js` — gotoNext() |
| `actionFinish` | POST | `/penjualan/finish` | `kitchen.screen.js` — showBtnPickup() |
| `actionTake` | POST | `/penjualan/take` | `kitchen.screen.js` — gotoPickup() |
| `actionPrintonelabel` | POST | `/penjualan/printonelabel` | `kitchen.screen.js` — klik qty |
| `actionSaveWip` | POST | `/penjualan/save-wip` | `kitchen.screen.js` — WIP modal |
| `actionHistoryCook` | GET | `/penjualan/history-cook` | `kitchen.screen.js` — History modal |
| `actionRecall` | POST | `/penjualan/recall` | `kitchen.screen.js` — Done transition |

### `formatResponseKitchen($model)` — Response Shape

Method ini di-share oleh `actionLoaditem`, `actionNeworder`, dan `actionUpdateitemstatus`.

```php
return [
    "penjualan"       => $model->penjualan,        // object Penjualan lengkap
    "sales_type_name" => $model->penjualan->salesType->name,
    "cat_name"        => $model->barang->katagori->top->nama,  // TopKatagori nama
    "unfinish"        => $cFinish,      // jumlah item di section ini yang belum selesai
    "item"            => $model,        // object PenjualanItem
    "barang"          => $model->barang,
    "jam"             => date("H:i", strtotime($model->penjualan->tanggal)),
    "timeTake"        => null | "22 Jan, 14:30",   // jadwal pickup (pre-order)
    "plainTimeTake"   => null | "2026-01-22 14:30:00",
    "tomorrow"        => date('ymd'),   // flag untuk pre-order besok
    "table_name"      => null | "Meja 5",
];
```

### Filter Item yang Ditampilkan (`actionLoaditem`)

Item **tidak ditampilkan** jika salah satu kondisi berikut terpenuhi:

1. `in_kitchen = 0` pada PenjualanItem (item tidak dikirim ke kitchen)
2. `isTake` tidak null (sudah di-pickup atau cancelled)
3. `bundle_status = 'parent'` (hanya `child` dan `null` yang ditampilkan)
4. Item tidak lolos filter `j_section` dan `barang_ids` user
5. Item ada di `barang_ids` user lain (eksklusif)
6. Order dari hari sebelumnya tanpa `time_to_take` (pre-order tanpa jadwal)
7. Pre-order dengan `time_to_take` yang juga dari hari sebelumnya

---

## 7. Socket.IO Events

Socket diinisialisasi di `mainku.js`:

```javascript
var urlSocket = $('#urlSocket').text();   // dari hidden div di layout
var cabangId  = $('#divCabangId').text(); // dari hidden div di layout
const socket  = io.connect(urlSocket);
```

### Events yang Didengarkan (Client ← Server)

| Event | Trigger | Handler |
|-------|---------|---------|
| `notif_pesanan_baru{cabangId}` | Kasir submit order | `place_new_order_kitchen(data.id)` |
| `finish_take{cabangId}` | Order di-pickup | Update last pickup display |
| `update_status_masak{cabangId}` | Status masak berubah | Refresh panel terkait |
| `show_hp{cabangId}` | (Internal) | Console log saja |

### Events yang Dikirim (Client → Server, via PHP Controller)

| Event | Dikirim dari | Payload |
|-------|-------------|---------|
| `finish_take` | `actionTake()` | `{cbgId, data}` |
| `print_one_label` | `actionPrintonelabel()` | `{cabang_id, nota, id, data, print_label}` |
| `pesanan_baru` | `actionUpdatepay()` | `{cbgId, nota}` |

---

## 8. Struktur HTML Panel (x_panel)

Panel dibuat secara dinamis oleh `addPanel()` dan diisi baris baru oleh `addRow()` di `kitchen.screen.js`.

```html
<div id="pnlN{pjId}" class="x_panel panelN [bg-panel] [tomorrow|noTomorrow]">

  <!-- Header panel -->
  <div>
    <b onclick="goFlash({pjId})">#A001 / Dine In / Meja 5 . NamaCustomer</b>
    <div class="nav pull-right">
      <span class="time"><i class="fa fa-clock-o"></i> 14:30</span>
    </div>
    <!-- Hanya muncul jika pre-order dengan time_to_take -->
    <div class="pickedCss">
      Pick Up at 22 Jan, 14:30
      <div class="countPickup" id="dCou{pjId}"><!-- countdown --></div>
    </div>
    <div class="clearfix"></div>
  </div>

  <!-- Konten / item list -->
  <div class="x_content">
    <table id="tblN{pjId}" class="countries_list">
      <tbody>
        <tr id="trItem{itemId}" class="cItm{pjId}">

          <!-- Qty — klik untuk print label -->
          <td onclick="printThisLabel('{itemId}')"
              class="text-right cQtyN [cQtTomN]"
              style="font-size:30px;width:40px;padding:0 5px">
            2
          </td>

          <!-- Nama item + detail -->
          <td style="width:80%;padding-left:1px">
            <!-- Bundle parent (jika ada) -->
            <span style="font-size:11px;font-weight:bold">Bundle Name</span><br>
            <!-- Nama barang utama -->
            <b>Nasi Goreng Spesial</b>
            <!-- Kategori -->
            <span style="font-size:11px"> ● Makanan</span>
            <!-- Varian/bubble (opsional) -->
            <br>Extra Pedas
            <!-- Add-ons (opsional, dari j_addon JSON) -->
            <br>Tambah Telur
            <!-- Note (opsional) -->
            <br><span class="span-note"><i class="fa fa-file-o"></i> tanpa bawang</span>
            <!-- Cook timing -->
            <br><span style="font-size:11px"><i class="fa fa-hourglass-o"></i> Langsung</span>

            <!-- Tombol recall (hanya di kolom Finished) -->
            <div id="btn-callItm{pjId}" onclick="recall('{itemId}')">
              <a class="btn btn-sm btn-flat btn-warning"><i class="fa fa-bullhorn"></i></a>
            </div>
          </td>

          <!-- Action button -->
          <td class="td-btn">
            <!-- New Order → tombol Process (animated heartbeat) -->
            <span class="bProses{countdownId} [btnTomorrow]">
              <a onclick="gotoNext('{pjId}','{itemId}','1')"
                 class="animated infinite heartBeat btn btn-sm btn-default btn-new">
                <i class="fa fa-hand-o-up"></i><br>Process
              </a>
            </span>

            <!-- On Process → tombol Done -->
            <a onclick="gotoNext('{pjId}','{itemId}','2')"
               class="btn btn-sm btn-default btn-process">
              <i class="fa fa-thumbs-o-up"></i><br>Done
            </a>

            <!-- Finished → tombol PickUp (awalnya hilang) -->
            <div id="btn-cItm{pjId}" class="hilang" onclick="gotoPickup({pjId})">
              <a class="btn btn-sm btn-default btn-pickup btn-loader">
                <i class="fa fa-check"></i><br>Pick Up
              </a>
            </div>
          </td>

        </tr>
      </tbody>
    </table>
  </div>
</div>
```

### ID Convention

| Pattern | Contoh | Keterangan |
|---------|--------|------------|
| `pnlN{pjId}` | `pnlN42` | Panel New untuk penjualan_id=42 |
| `pnlP{pjId}` | `pnlP42` | Panel On Process untuk penjualan_id=42 |
| `pnlF{pjId}` | `pnlF42` | Panel Finished untuk penjualan_id=42 |
| `tblN{pjId}` | `tblN42` | Tabel di dalam panel New |
| `trItem{itemId}` | `trItem117` | Row untuk item penjualan_id=117 |
| `btn-cItm{pjId}` | `btn-cItm42` | Wrapper tombol PickUp (disembunyikan awalnya) |
| `dCou{pjId}` | `dCou42` | Div countdown untuk pre-order |

---

## 9. JavaScript Functions

### `kitchen.screen.js`

| Fungsi | Signature | Deskripsi |
|--------|-----------|-----------|
| `loadItem()` | `()` | Initial data fetch dari `/penjualan/loaditem`. Dipanggil saat page load. |
| `addPanel(d, symbol)` | `(responseObj, 'N'\|'P'\|'F')` | Buat panel baru di kolom yang sesuai |
| `addRow(d, symbol)` | `(responseObj, 'N'\|'P'\|'F')` | Tambahkan row item ke panel yang sudah ada |
| `gotoNext(pjId, itemId, status)` | `(string, string, '1'\|'2')` | Transisi status item. 1=Process, 2=Done |
| `gotoPickup(id)` | `(pjId)` | Mark order as picked up, hapus panel F |
| `showBtnPickup(id, isDariLoad)` | `(pjId, bool?)` | Tampilkan tombol PickUp jika semua item selesai |
| `countPanel()` | `()` | Hitung total qty di setiap kolom, update counter header |
| `checkForTone()` | `()` | **Hanya mematikan** audio (+ reset) jika tidak ada `.bProses`. Tidak pernah memulai audio. |
| `goFlash(id)` | `(pjId)` | Flash animation pada semua panel yang punya penjualan_id ini |
| `recall(itemId)` | `(itemId)` | Kirim notif recall ke cashier via `/penjualan/recall` |
| `togleTomorrow(fromInit)` | `(bool?)` | Toggle tampilan order besok. Default hidden. |
| `newOrderKitchen(data, pjId)` | `(array, pjId)` | Handle order baru dari socket. **Langsung play `notif` audio** sebelum render panel. |
| `idleLogout()` | `()` | Auto reload setelah 30s idle, dengan countdown 5 menit |
| `runSave()` | `()` | Submit WIP form |
| `showHistoryCook()` | `()` | Buka modal History dan fetch data |
| `playBeep()` | `(freq?, dur?, vol?)` | Play beep via Web Audio API (dioverride di layout) |
| `setCountDowntimer(divId, tTake)` | `(string, datetime)` | Countdown timer untuk pre-order pickup |
| `printThisLabel(id)` | `(itemId)` | POST ke `/penjualan/printonelabel` untuk print label |

### Fungsi Search (didefinisikan di `main_kitchen.php`)

| Fungsi | Signature | Deskripsi |
|--------|-----------|-----------|
| `filterKitchenPanels(term)` | `(string)` | Filter semua `.x_panel` secara DOM berdasarkan kata kunci. Show/hide panel secara real-time. |
| `clearKitchenSearch()` | `()` | Kosongkan input search dan reset semua panel ke tampilan penuh. |
| `window.applyKitchenFilter` | `()` | Re-terapkan filter saat ini. Dipanggil dari `addPanel()` dan `addRow()` setelah panel baru di-append ke DOM. |

**Kolom yang disearch oleh `filterKitchenPanels`:**

| Field | Sumber DOM |
|-------|-----------|
| `symbol + no_urut` | `<b>` pertama di header panel: `#A001 / ...` |
| `sales_type_name` | `<b>` header panel: `/ Dine In / ...` |
| `table_name` | `<b>` header panel: `... / Meja 5 . ...` |
| `penjualan.pembeli` | `<b>` header panel: `... . NamaCustomer` |
| `barang.nama` | `<b>` dalam `td:nth-child(2)` setiap item row |

### `mainku.js` (Socket handlers)

| Fungsi | Dipanggil oleh | Deskripsi |
|--------|---------------|-----------|
| `place_new_order_kitchen(id)` | `socket.on("notif_pesanan_baru")` | Fetch dan render order baru |
| `place_new_order_screen(nota)` | `socket.on("notif_pesanan_baru")` | Update screen display (bukan kitchen) |
| `place_new_order_cashier(id)` | `socket.on("notif_pesanan_baru")` | Update cashier display |

---

## 10. Layout: main_kitchen.php

Layout ini **hanya digunakan** oleh halaman kitchen. Tidak di-share dengan halaman lain.

### Toolbar Utama

```
[Logo]  [  🔍 Search order, item, table...  ×  ]  [WIP] [Refresh] [Expand] [Logout]
```

| Elemen | Fungsi |
|--------|--------|
| Search input | Filter real-time DOM — memanggil `filterKitchenPanels(value)` pada event `oninput` |
| Tombol `×` | `clearKitchenSearch()` — muncul hanya saat ada teks di input |
| WIP | Buka `#myModal` (form produksi WIP). Disembunyikan untuk role `checker`. |
| Refresh | `location.reload()` |
| Expand | Toggle fullscreen via Fullscreen API, icon berubah expand/compress |
| Logout | POST `/site/logout` |

### SubToolbar

```
[HH:MM:SS] │ [Section1 · Section2 | Menu1 · Menu2]  ← marquee jika overflow
```

- Jam digital 24 jam, update setiap detik (`startClock()`)
- Teks filter auto-scroll (marquee) jika lebih panjang dari container
- Klik teks → buka `#filterInfoModal` — menampilkan section & menu aktif user
- Marquee pause saat modal terbuka, resume saat modal tutup

### Hidden Data Divs

Layout meng-inject data yang dibutuhkan JS via hidden div:

```html
<div id='urlSocket'  class='hilang'>{io.url}</div>
<div id='divCabangId' class='hilang'>{cabang_id}</div>
<div id='divJabatan'  class='hilang'>{jabatan}</div>
<div id='urlNewOrder' class='hilang'>{url penjualan/neworder}</div>
```

JS `mainku.js` membaca ini untuk koneksi socket dan filtering.

### Audio & Web Audio API

```javascript
// Inisialisasi lazy — AudioContext dibuat saat pertama kali dibutuhkan
var _audioCtx = null;
var _audioUnlocked = false;

// Unlock pada interaksi user pertama (required browser policy)
document.addEventListener('click',      _unlockAudio, { once: true });
document.addEventListener('touchstart', _unlockAudio, { once: true });

// Override playBeep() dari kitchen.screen.js dengan implementasi Web Audio API
function playBeep(frequency = 880, duration = 0.18, volume = 0.4) { ... }
```

> **Catatan:** `kitchen.screen.js` mendefinisikan `playBeep()` menggunakan HTML `<audio>` element. Layout `main_kitchen.php` **meng-override** fungsi ini dengan implementasi Web Audio API yang lebih andal dan tidak memerlukan file audio.

### Asset Bundle (KitchenAsset)

```php
$css = [
    'font-awesome 4.6.2 (CDN)',
    'bootstrap 3.4.1 (CDN)',
    'plugin/animate-css/animate.css',
];
$js = [
    'socket.io 1.7.3 (CDN)',
    'js/mainku.js',
    'js/kitchen.screen.js',
    'js/myjsq.js',
    'bootstrap 3.4.1 JS (CDN)',
];
```

---

## 11. Audio System

Sistem audio kitchen menggunakan dua mekanisme yang terpisah:

### Notifikasi Order Baru (notif_kitchen.mp3)

Audio diputar **hanya saat `newOrderKitchen()` dipanggil** (order baru masuk via socket). Audio tidak diputar oleh event lain (Process, Done, PickUp, countPanel, dll.).

```javascript
// kitchen.screen.js — newOrderKitchen()
function newOrderKitchen(data, pjId) {
    animateCSS('fabell', 'swing', function(){});
    notif.currentTime = 0;  // reset ke awal
    notif.play();           // putar notif — SATU-SATUNYA tempat audio dimulai
    // ...
}

// checkForTone() — HANYA mematikan audio, tidak pernah memulai
function checkForTone() {
    var bunyikan = ($('.bProses').length > 0);
    if (!bunyikan) {
        notif.pause();
        notif.currentTime = 0;
    }
}
```

`notif.loop = false` (di-set di `setNotif()`), jadi audio berhenti sendiri setelah file selesai diputar. `checkForTone()` mematikan audio lebih awal jika semua order sudah di-klik Process.

### Beep Satu Kali (Web Audio API)

```javascript
// main_kitchen.php — override implementasi dari kitchen.screen.js
function playBeep(frequency, duration, volume) {
    if (!soundEnabled) return;
    // ... generate tone via AudioContext oscillator
}
```

`playBeep()` dipanggil saat sound toggle diaktifkan kembali (konfirmasi audio hidup).

> **Penting:** `kitchen.screen.js` mendefinisikan `playBeep()` menggunakan HTML `<audio>` element (`beep.play()`). Layout `main_kitchen.php` **meng-override** fungsi ini dengan Web Audio API. Karena JS layout di-load setelah `kitchen.screen.js`, versi Web Audio API yang aktif.

### AudioContext Unlock Policy

Browser modern memblokir `AudioContext` sebelum ada interaksi user. Layout menggunakan teknik "silent buffer" untuk unlock pada klik/tap pertama:

```javascript
document.addEventListener('click',      _unlockAudio, { once: true });
document.addEventListener('touchstart', _unlockAudio, { once: true });
```

Setelah unlock, semua panggilan audio berikutnya (termasuk dari socket event) berjalan normal.

### Sound Toggle

Tombol Sound di toolbar (saat ini di-comment di layout, bisa di-uncomment) mengontrol variabel global `soundEnabled` yang dicek oleh `playBeep()`.

---

## 12. WIP Modal

WIP (Work In Progress) adalah fitur untuk mencatat produksi bahan setengah jadi di dapur. Modal ini terpisah dari alur order.

**Trigger:** Tombol WIP di toolbar → `#myModal`

**Data yang ditampilkan:** `Barang` dengan:
- `jenis = JENIS_WIP`
- `cook_type = COOK_TYPE_WIP` (dimasak manual, bukan auto)
- `hasil_jadi IS NOT NULL`

**Form fields per row:**
- Cook Recipe (Select2, pilih WIP item)
- Result/recipe (auto-fill dari `hasil_jadi * hasil_jadi_qty`)
- Output Qty (auto-fill dari `hasil_jadi_qty`)
- Cook (input qty yang dimasak)
- Cook Result (auto-hitung: Cook × Result/recipe)

**Save:** POST `/penjualan/save-wip` → `MyStockManagement::saveWIP()` — mengurangi stok bahan baku dan menambah stok WIP.

---

## 13. CSS Class Reference

### Panel Classes

| Class | Diaplikasikan ke | Deskripsi |
|-------|-----------------|-----------|
| `.x_panel` | Panel container | Base card style — white, border-left, shadow |
| `.panelN` | Panel New Order | Border kiri oranye `#f97316` |
| `.panelP` | Panel On Process | Border kiri biru `#3b82f6` |
| `.panelF` | Panel Finished | Border kiri hijau `#10b981`, bg hijau pucat |
| `.bg-panel` | Panel Finished | Alias untuk Finished (legacy dari JS) |
| `.tomorrow` | Pre-order besok | Muted, opacity 80%, border abu |
| `.noTomorrow` | Order hari ini | Show/hide oleh `togleTomorrow()` |

### Item Row Classes

| Class | Elemen | Deskripsi |
|-------|--------|-----------|
| `.cItm{pjId}` | `<tr>` | Selector untuk semua item dalam satu penjualan. `showBtnPickup()` cek `.cItm{id}.length == 0` |
| `.cQtyN` | `<td>` qty | Qty di kolom New. Disum oleh `countPanel()` |
| `.cQtyP` | `<td>` qty | Qty di kolom Process |
| `.cQtyF` | `<td>` qty | Qty di kolom Finished |
| `.cQtTomN/P/F` | `<td>` qty | Qty untuk pre-order besok (dikurangi dari total) |
| `.bProses{dCouId}` | Wrapper tombol | Selector untuk `checkForTone()`. Hidden via countdown timer |
| `.btnTomorrow` | Wrapper tombol | `display: none` — tombol process pre-order disembunyikan |
| `.span-note` | `<span>` | Teks note merah pada item |
| `.td-btn` | `<td>` | Cell untuk action button |

### Button Classes

| Class | Status | Warna |
|-------|--------|-------|
| `.btn-new` | New → Process | Oranye gradient (`#fb923c → #ea580c`) |
| `.btn-process` | Process → Done | Biru gradient (`#60a5fa → #2563eb`) |
| `.btn-pickup` | Finished → PickUp | Hijau gradient (`#34d399 → #059669`) |

### Layout Classes

| Class | Lokasi | Deskripsi |
|-------|--------|-----------|
| `.kitchen-toolbar` | Layout | Flexbox toolbar utama 64px |
| `.kitchen-subToolbar` | Layout | SubToolbar biru 30px |
| `.subToolbar-clock` | SubToolbar | Jam digital realtime |
| `.subToolbar-text` | SubToolbar | Teks filter, marquee jika overflow |
| `.subToolbar-text.is-marquee` | SubToolbar | Mengaktifkan CSS animation scroll |
| `.toolbar-search` | Toolbar | Pill input search — flex, max-width 260px, semi-transparan |
| `.toolbar-search-clear` | Toolbar | Tombol `×` clear search (`display:none` saat kosong) |
| `.kitchen-search-active` | `.right_col` | Diset saat filter aktif, mengaktifkan transition opacity pada `.x_panel` |
| `.toolbar-btn` | Toolbar | Tombol icon+text di toolbar |
| `.toolbar-btn-form` | Toolbar | Form wrapper tombol logout |
| `.bgHeaderApur` | Layout | Container header biru gelap `#01273e` |

---

## Catatan Penting untuk Developer

1. **Jangan hapus hidden divs** (`#urlSocket`, `#divCabangId`, dll.) dari layout. JS `mainku.js` bergantung pada ini untuk socket connection dan filtering.

2. **`buble` / `buble2`** di response adalah varian item (misal: level es). Field ini nullable.

3. **`j_addon`** adalah JSON string array di `PenjualanItem` yang berisi add-on item (topping, dll.).

4. **`cook_timing`** di `PenjualanItem`: `0 = Langsung`, `1 = Panggilan`. Tampil di item name tapi tidak mempengaruhi logika status.

5. **Duplicate prevention di `addRow()`:** Sebelum append row, cek `$('#trItem{id}').length`. Jika sudah ada, skip. Ini mencegah duplikasi saat socket event terlambat.

6. **`idleLogout()`** auto-reload halaman setelah 30 detik tidak ada aktivitas mouse/touch/keyboard + 5 menit countdown. Ini memastikan halaman kitchen selalu fresh dan session tidak expired.

7. **`gotoPickup()` mengubah class** panel menjadi `x_panel` saja (hapus `panelF bg-panel`) sebelum animasi keluar — ini adalah reset class agar tidak ada styling conflict saat animasi `bounceOutUp`.

8. **`playBeep()` di-override:** Layout mendefinisikan ulang `playBeep()` setelah `kitchen.screen.js` di-load. Versi layout menggunakan Web Audio API yang tidak memerlukan interaksi user (setelah unlock pertama).

9. **Audio hanya dari `newOrderKitchen()`:** `checkForTone()` tidak lagi memulai audio — ia hanya menghentikannya. Satu-satunya titik di mana `notif.play()` dipanggil adalah `newOrderKitchen()`. Ini mencegah audio berbunyi saat page-load atau saat transisi status (Process/Done).

10. **Search filter re-apply otomatis:** `addPanel()` dan `addRow()` memanggil `window.applyKitchenFilter()` setelah setiap DOM update. Ini memastikan panel yang baru masuk via socket langsung disembunyikan jika tidak cocok dengan kata kunci yang sedang aktif di search bar.

11. **Search bekerja pada teks yang ter-render di DOM**, bukan pada data objek JS. Jika format teks di header panel atau nama item berubah di `addPanel()`/`addRow()`, pastikan selector `filterKitchenPanels` masih tepat mengarah ke elemen yang benar.

---

*Dokumen ini ditulis berdasarkan kode aktual per 2026-05-29. Terakhir diperbarui: 2026-05-29 (audio behavior, search filter).*
