> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quinnsambal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pos order queue

<style>
  {`
    [class*="max-w-none"][class*="table"] {
      display: block !important;
      overflow-x: auto !important;
      max-width: 100% !important;
      width: 100% !important;
      flex-grow: 0 !important;
    }
    [class*="max-w-none"][class*="table"] > table {
      width: 100% !important;
      max-width: 100% !important;
      table-layout: fixed !important;
    }
    .mermaid {
      max-width: 100% !important;
      overflow-x: auto !important;
    }
    article svg[role="img"] {
      max-width: 100% !important;
      height: auto !important;
    }
    article img, .prose img {
      max-width: 100% !important;
      height: auto !important;
    }
    img[src*="LOGO"], img[src*="logo"] {
      max-width: 120px !important;
      max-height: 40px !important;
      width: auto !important;
      height: auto !important;
      object-fit: contain !important;
    }
    `}
</style>

***

title: "POS Order Queue"
description: "Live fulfillment queue SNISHOP ERP — Kanban board real-time, kitchen ticket printing, browser push notification, dan multi-channel order tracking."
-----------------------------------------------------------------------------------------------------------------------------------------------------------------

# POS Order Queue

<img src="https://mintlify.s3.us-west-1.amazonaws.com/quinnofspicy/docs/mintlify/screenshots/pos/order-queue.png" alt="POS Order Queue" />

Halaman **POS Order Queue** adalah live fulfillment board yang menampilkan semua pesanan aktif dalam format **Kanban 3 kolom**. Halaman ini dirancang untuk tim dapur/kitchen dan staff fulfillment agar bisa melihat dan memproses pesanan secara real-time dari semua channel penjualan.

## Arsitektur Komponen

```mermaid theme={null}
graph TB
    subgraph QUEUE["POSOrderQueue.jsx — 862 lines"]
        KANBAN["Kanban Board<br/>3 Columns: Pending / Processing / Completed"]
        CARD["OrderCard<br/>Channel badge, items, timer"]
        MODAL["OrderDetailModal<br/>Full order info, actions"]
        PRINT["Kitchen Ticket<br/>Bluetooth thermal printer"]
        NOTIFY["Browser Push<br/>+ Sound alert"]
    end

    subgraph DATA["Data Sources"]
        SF["listPOSOrderQueue<br/>Server Function"]
        BC["BroadcastChannel<br/>snishop_order_queue"]
        WS["WebSocket Subscribe<br/>CompanyPOSTransaction, QuinnOrder"]
        POLL["60s Polling<br/>Tab-visible only"]
    end

    subgraph ENTITIES["Entities"]
        CPT["CompanyPOSTransaction"]
        QO["QuinnOrder"]
    end

    SF --> KANBAN
    BC --> KANBAN
    WS --> KANBAN
    POLL --> KANBAN
    KANBAN --> CARD
    CARD --> MODAL
    MODAL --> PRINT
    MODAL --> NOTIFY
    CPT --> SF
    QO --> SF
```

## Kanban Board — 3 Kolom

```mermaid theme={null}
graph LR
    subgraph PENDING["🟡 Pending"]
        P1["Order #001<br/>3 items<br/>02:34"]
        P2["Order #002<br/>1 item<br/>00:45"]
    end

    subgraph PROCESSING["🔵 Processing"]
        PR1["Order #003<br/>5 items<br/>08:12"]
        PR2["Order #004<br/>2 items<br/>03:55"]
    end

    subgraph COMPLETED["🟢 Completed"]
        C1["Order #005<br/>4 items<br/>Done"]
        C2["Order #006<br/>1 item<br/>Done"]
    end

    PENDING -->|Mulai proses| PROCESSING
    PROCESSING -->|Selesai| COMPLETED
```

| Kolom | Status Filter | Warna | Timer | Aksi |
| - | - | - | - | - |
| **Pending** | `order_status = 'pending'` | Kuning | Waktu sejak order dibuat | Start Processing |
| **Processing** | `order_status = 'processing'` | Biru | Waktu sejak mulai diproses | Mark Completed |
| **Completed** | `order_status = 'completed'` | Hijau | — | View Detail |

## OrderCard — Informasi per Order

Setiap OrderCard menampilkan:

| Elemen | Deskripsi | Visual |
| - | - | - |
| **Transaction Number** | `TRX-20261010-ABC123` | Bold header |
| **Channel Badge** | Sumber order | Color-coded pill |
| **Order Type Badge** | Dine In / Takeaway / Delivery | Icon + text |
| **Item Count** | Jumlah item dalam order | Badge |
| **Timer** | Waktu sejak order dibuat | MM:SS format |
| **Customer Name** | Nama customer (jika ada) | Subtitle |
| **Items Preview** | 3 item pertama | Truncated list |

### Channel Badge Colors

| Channel | Warna | Emoji |
| - | - | - |
| Offline POS | Hijau | 🏪 |
| GrabFood | Hijau Tua | 🏍️ |
| Marketplace | Oranye | 🛒 |
| WhatsApp | Hijau | 💬 |
| Website | Biru | 🌐 |
| B2B | Ungu | 🏢 |
| Reseller | Pink | 🤝 |
| Social Media | Merah | 📱 |

### Order Type Badges

| Type | Icon | Deskripsi |
| - | - | - |
| **Dine In** | 🍽️ | Makan di tempat |
| **Takeaway** | 📦 | Bungkus/bawa pulang |
| **Delivery** | 🚗 | Diantar ke alamat |

## Data Fetching Strategy

POSOrderQueue menggunakan **4 sumber data** secara bersamaan:

```mermaid theme={null}
sequenceDiagram
    participant Component
    participant ServerFn
    participant BroadcastChannel
    participant WebSocket
    participant Polling

    Note over Component: Mount
    Component->>ServerFn: listPOSOrderQueue()
    ServerFn-->>Component: Initial orders

    Note over BroadcastChannel: Real-time
    BroadcastChannel->>Component: snishop_order_queue event
    Note over Component: Update order list

    Note over WebSocket: Server push
    WebSocket->>Component: CompanyPOSTransaction changed
    Note over Component: Refetch orders

    Note over Polling: Every 60s (tab visible)
    Polling->>ServerFn: listPOSOrderQueue()
    ServerFn-->>Component: Refreshed orders
```

| Source | Trigger | Latency | Fallback |
| - | - | - | - |
| **Server Function** | Initial load | \~300ms | Error state |
| **BroadcastChannel** | `snishop_order_queue` event | \< 100ms | — |
| **WebSocket** | `CompanyPOSTransaction` / `QuinnOrder` subscribe | \< 500ms | Polling |
| **Polling** | 60s interval (tab visible only) | \~300ms | — |

**Optimasi:** Polling hanya aktif ketika tab browser visible (menggunakan `document.visibilityState`). Ketika tab di-background, polling berhenti untuk menghemat bandwidth.

## New Order Detection

Sistem mendeteksi order baru dan memberikan notifikasi:

```mermaid theme={null}
graph TB
    A["Order baru masuk"] --> B["knownOrderIdsRef check"]
    B --> C{ID sudah dikenal?}
    C -->|Tidak| D["🆕 New order detected"]
    D --> E["Browser Push Notification"]
    D --> F["Sound Alert"]
    D --> G["Update knownOrderIdsRef"]
    C -->|Ya| H["Existing order update"]
    H --> I["Silent refresh"]
```

**Implementasi:**

* `knownOrderIdsRef` menyimpan Set dari order IDs yang sudah dilihat
* Setiap fetch baru, bandingkan dengan ref
* Order ID yang tidak ada di ref = order baru
* Trigger browser notification + sound
* Update ref setelah notifikasi

## Kitchen Ticket Printing

Cetak tiket dapur untuk setiap order menggunakan Bluetooth thermal printer:

**Format Tiket:**

```
================================
         KITCHEN TICKET
================================
Order: TRX-20261010-ABC123
Type:  Dine In
Time:  14:30:00
--------------------------------
ITEMS:
  1x Sambal Matah Premium
  2x Nasi Goreng Pedas
  1x Es Teh Manis
--------------------------------
Customer: Budi Santoso
Notes: Tidak terlalu pedas
================================
```

**Implementasi:**

* Bluetooth thermal printer (58mm / 80mm)
* Auto-print option di settings
* Manual print dari OrderDetailModal
* `isPrintingSlip` state untuk prevent double-print

## Browser Push Notification

Ketika order baru masuk, sistem menampilkan notifikasi browser:

**Notification Content:**

* **Title:** "Pesanan Baru!"
* **Body:** "Order #TRX-xxx dari \[Channel] - \[N] items"
* **Icon:** App logo
* **Sound:** Default notification sound

**Permission:**

* Meminta izin Notification API saat pertama kali
* User bisa disable di browser settings
* Fallback ke sound alert jika notification ditolak

## Filter & Search

### Filter Controls

| Filter | Options | Default |
| - | - | - |
| **Search** | Transaction number, customer name | Empty |
| **Category** | All, Food, Beverage, Snack, Other | All |
| **Channel** | All, Offline, Grab, Marketplace, WhatsApp, Website, B2B, Reseller, Social | All |
| **Order Type** | All, Dine In, Takeaway, Delivery | All |

### Filter Behavior

* Semua filter bisa dikombinasikan
* Filter persist selama sesi halaman
* Reset filter: klik "Clear All"
* Result count ditampilkan di header

## Order Detail Modal

Klik order card untuk membuka detail lengkap:

**Tab 1: Order Info**

* Transaction number & date
* Channel & order type badges
* Customer info (name, phone, address)
* Notes from customer

**Tab 2: Items**

* Full item list dengan quantity dan price
* Item notes (contoh: "tidak pakai bawang")
* Subtotal per item

**Tab 3: Payment**

* Payment method & status
* Total amount
* Split payment breakdown (jika ada)

**Tab 4: Actions**

* Start Processing / Mark Completed
* Print Kitchen Ticket
* Print Receipt
* Cancel Order
* Send WhatsApp

## Server Functions

| Function | Purpose | Parameters |
| - | - | - |
| `listPOSOrderQueue` | Fetch all active orders | company\_id, status\_filter |
| `transitionPOSOrderStatus` | Update order status | order\_id, new\_status, user\_id |

### transitionPOSOrderStatus Workflow

```mermaid theme={null}
sequenceDiagram
    participant UI
    participant API
    participant DB
    participant BC

    UI->>API: transitionPOSOrderStatus(id, 'processing')
    API->>DB: Update order_status
    DB->>API: Success
    API->>BC: Broadcast snishop_order_queue
    BC->>UI: All clients update
    API->>UI: Return updated order
```

## Demo Mode

POSOrderQueue mendukung demo mode untuk presentasi:

**Demo Data:**

* `DEMO_ORDERS`: 6 sample orders dengan berbagai channel dan status
* `DEMO_COMPANY`: Dummy company context
* Auto-rotate orders untuk simulasi real-time

**Activation:**

* Otomatis ketika `isDemoMode` = true
* Tidak ada data real yang dimodifikasi
* Semua aksi (process, complete) bekerja di memory saja

## Performance Optimization

| Optimization | Implementation | Impact |
| - | - | - |
| **Tab-visible polling** | `document.visibilityState` check | 50% less API calls |
| **Known order tracking** | `knownOrderIdsRef` Set | Prevent duplicate notifications |
| **Error recovery** | `loadError` + `actionError` states | Graceful degradation |
| **Skeleton loading** | `QueueSkeleton` component | Better perceived performance |
| **Empty state** | `EmptyState` component | Clear UX when no orders |

## Cross-Module Integration

```mermaid theme={null}
graph LR
    subgraph QUEUE["Order Queue"]
        OQ["POSOrderQueue"]
    end

    subgraph CASHIER["Cashier"]
        CPC["CompanyPOSCashier"]
    end

    subgraph ORDERMGMT["Order Management"]
        OM["OrderManagement"]
    end

    subgraph REPORTS["Reports"]
        PR["POSReports"]
    end

    CPC -->|snishop_order_queue| OQ
    OM -->|snishop_order_queue| OQ
    OQ -->|View detail| CPC
    OQ -->|Completed data| PR
```

## Best Practices

### Untuk Kitchen Staff

1. **Pantau timer** — order yang sudah > 15 menit perlu prioritas
2. **Print kitchen ticket** untuk setiap order baru
3. **Update status tepat waktu** — jangan biarkan order stuck di pending
4. **Cek notes** — ada instruksi khusus dari customer
5. **Komunikasi** dengan kasir jika ada item yang habis

### Untuk Manager

1. **Monitor queue** dari dashboard untuk melihat bottleneck
2. **Set target time** — pending \< 5 menit, processing \< 15 menit
3. **Review completed orders** untuk quality check
4. **Analisis peak hours** dari POS Reports
5. **Adjust staffing** berdasarkan order volume per jam

### Untuk Developer

1. **Selalu gunakan `listPOSOrderQueue`** untuk fetch data (bukan direct entity filter)
2. **Listen ke `snishop_order_queue`** untuk real-time updates
3. **Handle error state** dengan graceful UI
4. **Optimize polling** — hanya ketika tab visible
5. **Test demo mode** untuk presentasi tanpa data real

***

## Entity Relationship Diagram — Order Queue

Berikut adalah **ER Diagram** yang menggambarkan relasi antar-entitas yang terlibat langsung dalam alur Order Queue di SNISHOP ERP. Diagram ini membantu developer dan stakeholder memahami bagaimana data pesanan mengalir dari berbagai channel penjualan ke dapur dan fulfillment.

```mermaid theme={null}
erDiagram
    CompanyPOSTransaction {
        string company_id FK "ID perusahaan (wajib)"
        string location_id FK "ID lokasi gudang/toko"
        string location_name "Nama lokasi"
        string transaction_number UK "Nomor transaksi unik (wajib)"
        string invoice_number "Nomor invoice"
        datetime transaction_date "Tanggal transaksi"
        array items "Embedded line items (wajib)"
        number subtotal "Subtotal sebelum diskon dan pajak"
        number discount_amount "Nilai diskon (default 0)"
        number discount_percentage "Persentase diskon (default 0)"
        number tax_amount "PPN — inclusive atau exclusive"
        number total "Total akhir (wajib)"
        string payment_method "cash|card|transfer|ewallet|qris|..."
        array payments "Split payment detail"
        string payment_status "pending|paid|failed|refunded|..."
        string customer_id FK "ID member (opsional)"
        string customer_name "Nama pelanggan"
        string customer_phone "Nomor telepon pelanggan"
        string customer_address "Alamat pelanggan"
        number points_earned "Poin member didapat"
        number points_used "Poin member dipakai"
        string cashier_id FK "ID karyawan kasir"
        string cashier_name "Nama kasir"
        string assigned_to_id FK "Staff yang menangani order"
        string assigned_to_name "Nama staff penugasan"
        string notes "Catatan pesanan"
        string source "pos|online_catalog|website|marketplace|..."
        string order_status "pending|confirmed|preparing|ready|served|completed|cancelled|voided"
        string sales_channel "offline_pos|website|marketplace|whatsapp|..."
        string tracking_number "No. resi pengiriman"
        string shipping_address "Alamat pengiriman"
        object metadata "Data tambahan (online order info)"
        datetime verified_at "Waktu verifikasi pembayaran"
        string verified_by "User yang memverifikasi"
        datetime processed_at "Waktu mulai diproses"
        datetime cancelled_at "Waktu pembatalan"
    }

    CompanyPOSProduct {
        string company_id FK "ID perusahaan (wajib)"
        string name "Nama produk (wajib)"
        string sku UK "SKU/Barcode (wajib)"
        string category "Kategori produk"
        string category_key "Canonical: kecil|besar|pouch|bundle|snack"
        string variant_key "Canonical: original|extra_spicy|bundle"
        number price "Harga jual (wajib)"
        number cost "Harga modal/beli"
        number stock "Stok saat ini (default 0)"
        number min_stock "Minimum stok alert (default 5)"
        string image_url "URL gambar utama"
        array gallery "Galeri foto (maks 20)"
        array variants "Varian produk"
        boolean is_active "Status aktif (default true)"
        string product_type "finished_good|raw_material|semi_finished|..."
        boolean is_bundle "Paket bundle virtual (default false)"
        array bundle_components "Komponen SKU bundle"
        object channel_pricing "Harga per channel penjualan"
        number sold_count "Total produk terjual (default 0)"
        boolean is_locked "Terkunci karena ada transaksi"
    }

    CompanyPOSCategory {
        string company_id FK "ID perusahaan (wajib)"
        string name "Nama kategori (wajib)"
        string description "Deskripsi kategori (maks 1000)"
        string icon "Icon emoji"
        string color "Warna hex (default #3b82f6)"
        number order "Urutan tampil (default 0)"
    }

    CompanyPOSInventory {
        string company_id FK "ID perusahaan (wajib)"
        string product_id FK "ID produk (wajib)"
        string product_name "Nama produk"
        string type "in|out|adjustment (wajib)"
        number quantity "Jumlah perubahan (wajib)"
        number stock_before "Stok sebelum pergerakan"
        number stock_after "Stok setelah pergerakan"
        string reason "Alasan pergerakan stok"
        string reference_id FK "ID transaksi terkait"
        string performed_by "User yang melakukan"
    }

    QuinnOrder {
        string company_id FK "ID perusahaan (auto-linked)"
        string checkout_request_id UK "Idempotency key dari storefront"
        string pos_transaction_id FK "Mirror ke CompanyPOSTransaction"
        string order_number UK "Nomor pesanan pelanggan"
        string customer_name "Nama pelanggan (wajib)"
        string customer_phone "WhatsApp pelanggan (wajib)"
        string customer_email "Email pelanggan"
        string shipping_address "Alamat pengiriman (wajib)"
        string city "Kota tujuan"
        string notes "Catatan tambahan"
        array items "Daftar produk (wajib)"
        number subtotal "Total sebelum ongkir"
        number shipping_cost "Biaya pengiriman"
        number total "Total akhir (wajib)"
        string payment_method "transfer_bank|cod|whatsapp"
        string status "pending|confirmed|processing|shipped|completed|cancelled"
        string payment_status "unpaid|pending_verification|paid|..."
        string fulfillment_status "unprocessed|processing|ready|shipped|..."
        string reservation_status "active|consumed|released|expired"
        string sales_channel "Kanal penjualan (default website)"
        object shipping_address_snapshot "Snapshot alamat lengkap"
        string payment_proof_url "URL bukti transfer"
        array payment_proofs "Histori bukti transfer"
        array status_history "Histori perubahan status"
    }

    %% Relasi utama
    CompanyPOSTransaction ||--o{ CompanyPOSInventory : "setiap transaksi menulis log inventory (out)"
    CompanyPOSTransaction }o--|| CompanyPOSCategory : "items[].category merujuk kategori"
    CompanyPOSTransaction }o--o| QuinnOrder : "QuinnOrder mirror ke POS Transaction via pos_transaction_id"
    CompanyPOSProduct }o--|| CompanyPOSCategory : "produk milik satu kategori"
    CompanyPOSProduct ||--o{ CompanyPOSInventory : "setiap produk punya log pergerakan stok"
    CompanyPOSTransaction }o--o{ CompanyPOSProduct : "items[].product_id merujuk produk"
    QuinnOrder }o--o{ CompanyPOSProduct : "items[].sku merujuk produk"
```

### Penjelasan Relasi (Bahasa Indonesia)

1. **CompanyPOSTransaction → CompanyPOSProduct**: Setiap transaksi POS menyimpan array `items[]` yang merujuk ke `product_id` dari `CompanyPOSProduct`. Ini adalah relasi many-to-many melalui embedded document.
2. **CompanyPOSTransaction → CompanyPOSInventory**: Setiap transaksi yang berhasil otomatis menulis log `CompanyPOSInventory` dengan `type = 'out'` untuk mengurangi stok.
3. **CompanyPOSTransaction → QuinnOrder**: Order dari website Quinn di-mirror ke `CompanyPOSTransaction` melalui field `pos_transaction_id`. Ini memungkinkan laporan terpadu dari semua channel.
4. **CompanyPOSProduct → CompanyPOSCategory**: Setiap produk POS dikelompokkan dalam satu kategori untuk filtering dan reporting.
5. **QuinnOrder → CompanyPOSProduct**: Item pada QuinnOrder merujuk ke SKU produk yang sama, memastikan konsistensi inventory.

***

## Entity Schema Tables — Order Queue

Tabel berikut mendefinisikan seluruh field penting pada entitas yang terlibat dalam Order Queue, beserta tipe data, constraint, dan deskripsi bisnisnya.

### CompanyPOSTransaction — Skema Field Utama

| Field | Tipe | Enum / Constraint | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | `string` | **wajib** (FK) | — | ID perusahaan. Multi-tenant isolation. |
| `location_id` | `string` | FK | — | ID lokasi gudang/toko tempat transaksi terjadi. |
| `location_name` | `string` | — | — | Nama lokasi (denormalized untuk performa). |
| `transaction_number` | `string` | **wajib**, unique | — | Format: `TRX-YYYYMMDD-XXXXXX`. Nomor transaksi unik. |
| `invoice_number` | `string` | — | — | Nomor invoice yang dicetak untuk pelanggan. |
| `transaction_date` | `datetime` | — | — | Timestamp transaksi dibuat. |
| `items` | `array<object>` | **wajib** | — | Daftar produk yang dipesan (lihat sub-tabel di bawah). |
| `subtotal` | `number` | — | — | Total harga sebelum diskon dan pajak. |
| `discount_amount` | `number` | — | `0` | Nilai diskon dalam Rupiah. |
| `discount_percentage` | `number` | — | `0` | Persentase diskon (0–100). |
| `tax_amount` | `number` | — | `0` | PPN. Mode inclusive: sudah termasuk di total. Mode exclusive: tambahan di atas total. |
| `total` | `number` | **wajib** | — | Total akhir yang harus dibayar. |
| `payment_method` | `string` | enum | — | Metode pembayaran (lihat enum di bawah). |
| `payments` | `array<object>` | — | — | Detail split payment (multi-tender). |
| `payment_status` | `string` | enum | `"pending"` | Status pembayaran. |
| `order_status` | `string` | enum | `"pending"` | **Status order untuk Kanban Queue** (lihat 8 nilai di bawah). |
| `source` | `string` | enum | `"pos"` | Sumber transaksi (channel asal). |
| `sales_channel` | `string` | enum | `"offline_pos"` | Kanal penjualan ternormalisasi. |
| `customer_id` | `string` | FK | — | ID member (jika pelanggan terdaftar). |
| `customer_name` | `string` | — | — | Nama pelanggan. |
| `customer_phone` | `string` | — | — | Nomor telepon/WhatsApp pelanggan. |
| `customer_address` | `string` | — | — | Alamat lengkap pelanggan. |
| `cashier_id` | `string` | FK | — | ID karyawan kasir yang memproses. |
| `cashier_name` | `string` | — | — | Nama kasir. |
| `assigned_to_id` | `string` | FK | — | ID staff dapur/fulfillment yang ditugaskan. |
| `assigned_to_name` | `string` | — | — | Nama staff penugasan. |
| `notes` | `string` | — | — | Catatan khusus dari pelanggan/kasir. |
| `points_earned` | `number` | — | `0` | Poin membership yang didapat dari transaksi ini. |
| `points_used` | `number` | — | `0` | Poin membership yang dipakai sebagai diskon. |
| `metadata` | `object` | — | — | Data tambahan (info online order, metadata channel, dll). |
| `processed_at` | `datetime` | — | — | Timestamp saat order mulai diproses di dapur. |
| `cancelled_at` | `datetime` | — | — | Timestamp saat order dibatalkan. |
| `verified_at` | `datetime` | — | — | Timestamp verifikasi pembayaran. |
| `verified_by` | `string` | — | — | User yang memverifikasi pembayaran. |

### Enum: `order_status` — 8 Nilai Status Order Queue

Field `order_status` pada `CompanyPOSTransaction` adalah **sumber kebenaran** untuk posisi order di Kanban board. Berikut 8 nilai yang didukung:

| # | Nilai | Kolom Kanban | Warna | Deskripsi Bisnis |
| - | - | - | - | - |
| 1 | `pending` | 🟡 Pending | Kuning | Order baru diterima dari kasir/channel, belum mulai diproses. Antrian menunggu dapur. |
| 2 | `confirmed` | 🟡 Pending | Kuning | Order sudah dikonfirmasi oleh kasir/staff — item tersedia, siap masuk dapur. |
| 3 | `preparing` | 🔵 Processing | Biru | Dapur sedang menyiapkan/memasak order. Timer processing berjalan. |
| 4 | `ready` | 🔵 Processing | Biru | Makanan sudah siap disajikan/diambil. Menunggu staff fulfillment. |
| 5 | `served` | 🔵 Processing | Biru | Order sudah dihidangkan ke pelanggan (Dine In) atau diserahkan ke kurir (Delivery). |
| 6 | `completed` | 🟢 Completed | Hijau | Order selesai — pelanggan sudah menerima, atau takeaway sudah diambil. |
| 7 | `cancelled` | — (dipisah) | Merah | Order dibatalkan oleh kasir/manager. Stok dikembalikan (inventory adjustment). |
| 8 | `voided` | — (dipisah) | Abu-abu | Order di-void (dihapus secara administratif). Biasanya karena kesalahan input. Berbeda dengan cancel — voided tidak mengurangi stok. |

### Enum: `payment_method` — Metode Pembayaran

| Nilai | Deskripsi |
| - | - |
| `cash` | Tunai di kasir |
| `card` | Kartu debit/kredit |
| `transfer` | Transfer bank manual |
| `ewallet` | E-wallet (GoPay, OVO, Dana, dll) |
| `qris` | QRIS scan |
| `saldo` | Saldo member/membership |
| `mayar` | Mayar payment gateway |
| `debt` | Hutang/piutang (tempo) |
| `manual_transfer` | Transfer manual (dengan verifikasi) |
| `midtrans` | Midtrans payment gateway |
| `tripay` | Tripay payment gateway |
| `stripe` | Stripe (international) |
| `paypal` | PayPal (international) |

### Enum: `payment_status` — Status Pembayaran

| Nilai | Deskripsi |
| - | - |
| `pending` | Belum ada pembayaran diterima. |
| `pending_verification` | Bukti transfer sudah diupload, menunggu verifikasi staf. |
| `partially_paid` | Pembayaran sebagian — ada sisa tagihan. |
| `paid` | Pembayaran lunas dan terverifikasi. |
| `failed` | Pembayaran gagal (gateway error, dll). |
| `refunded` | Dana dikembalikan ke pelanggan. |
| `rejected` | Bukti transfer ditolak oleh verifier. |

### Items Sub-Schema (Embedded dalam CompanyPOSTransaction)

| Field | Tipe | Deskripsi |
| - | - | - |
| `product_id` | `string` | FK ke `CompanyPOSProduct._id`. |
| `product_name` | `string` | Nama produk (denormalized snapshot). |
| `sku` | `string` | SKU/barcode produk saat transaksi. |
| `quantity` | `number` | Jumlah yang dipesan. |
| `price` | `number` | Harga satuan saat transaksi. |
| `discount` | `number` | Diskon per-baris (default 0). |
| `subtotal` | `number` | Subtotal baris = `quantity * price - discount`. |
| `unit` | `string` | Satuan produk (pcs, gram, dll). |
| `unit_cost` | `number` | HPP per unit saat transaksi (untuk kalkulasi laba). |
| `total_cost` | `number` | Total HPP = `quantity * unit_cost`. |
| `note` | `string` | Catatan per-baris (contoh: "tidak pakai bawang"). |

### QuinnOrder — Skema Field (Channel Website)

| Field | Tipe | Enum / Constraint | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | `string` | FK | — | Auto-linked dari storefront Quinn. |
| `checkout_request_id` | `string` | unique | — | Idempotency key dari checkout publik. |
| `pos_transaction_id` | `string` | FK | — | Mirror ke `CompanyPOSTransaction._id`. |
| `order_number` | `string` | unique | — | Nomor pesanan yang ditampilkan ke pelanggan. |
| `customer_name` | `string` | **wajib** | — | Nama lengkap pelanggan. |
| `customer_phone` | `string` | **wajib** | — | Nomor WhatsApp pelanggan. |
| `customer_email` | `string` | — | — | Email pelanggan. |
| `shipping_address` | `string` | **wajib** | — | Alamat lengkap pengiriman. |
| `city` | `string` | — | — | Kota tujuan pengiriman. |
| `items` | `array<object>` | **wajib** | — | Produk yang dipesan (sku, name, size, price, quantity, subtotal). |
| `subtotal` | `number` | — | `0` | Total harga produk sebelum ongkir. |
| `shipping_cost` | `number` | — | `0` | Biaya pengiriman. |
| `total` | `number` | **wajib** | `0` | Total akhir = subtotal + shipping\_cost. |
| `payment_method` | `string` | enum | `"whatsapp"` | `transfer_bank`, `cod`, `whatsapp`. |
| `status` | `string` | enum | `"pending"` | Status pesanan (6 nilai). |
| `payment_status` | `string` | enum | `"unpaid"` | Status pembayaran terverifikasi. |
| `fulfillment_status` | `string` | enum | `"unprocessed"` | Status pemenuhan barang fisik. |
| `reservation_status` | `string` | enum | `"active"` | Status reservasi stok inventori. |
| `payment_proofs` | `array<object>` | — | — | Histori bukti transfer yang diupload pelanggan. |
| `status_history` | `array<object>` | — | — | Histori perubahan status (audit trail). |

***

## State Diagram — Order Status Lifecycle (Kanban Queue)

Diagram berikut menggambarkan **seluruh siklus hidup status order** di dalam POS Order Queue. Setiap transisi hanya boleh dilakukan oleh role tertentu (lihat RBAC table di bawah).

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending : Order baru masuk dari Kasir / Channel Online

    state "Kolom PENDING" as pending_group {
        pending --> confirmed : Kasir konfirmasi order\n(item tersedia, validasi)
        pending --> cancelled : Kasir/Manager batalkan\n(stok belum terpengaruh)
        pending --> voided : Admin void\n(kesalahan input)
    end

    state "Kolom PROCESSING" as processing_group {
        confirmed --> preparing : Dapur mulai proses\n(assigned_to_id di-set)
        preparing --> ready : Dapur selesai masak\n(kitchen ticket di-print)
        ready --> served : Staff hidangkan / serahkan kurir
        served --> completed : Pelanggan terima /\ntakeaway diambil
    }

    state "Kolom COMPLETED" as completed_group {
        completed --> [*] : Order selesai —\nmasuk laporan & reporting
    }

    state "Status Terminal (Dipisah)" as terminal_group {
        cancelled --> [*] : Stok dikembalikan\n(via inventory adjustment)
        voided --> [*] : Tidak mempengaruhi stok\n(administratif saja)
    end

    %% Transisi darurat
    confirmed --> cancelled : Dapur batalkan\n(item habis)
    preparing --> cancelled : Dapur batalkan\n(kendala masak)
    ready --> cancelled : Pelanggan batalkan\n(sebelum served)

    %% Catatan transisi
    note right of pending_group : Timer berjalan sejak order dibuat\nTarget: < 5 menit di kolom ini
    note right of processing_group : Timer berjalan sejak status = preparing\nTarget: < 15 menit di kolom ini
    note right of terminal_group : Cancelled & Voided tidak tampil\ndi Kanban board utama
```

### Penjelasan Transisi Status (Bahasa Indonesia)

| Dari → Ke | Trigger | Siapa | Catatan |
| - | - | - | - |
| `[*] → pending` | Order baru dibuat di kasir atau masuk dari channel online | Kasir / Sistem | Otomatis. Order muncul di kolom Pending. |
| `pending → confirmed` | Kasir mengkonfirmasi bahwa item tersedia dan order valid | Kasir | Trigger: tombol "Confirm" di OrderDetailModal. |
| `pending → cancelled` | Order dibatalkan sebelum mulai diproses | Kasir / Manager | Stok belum terpengaruh karena belum ada inventory out. |
| `pending → voided` | Kesalahan input data order (salah item, salah harga) | Admin | Void bersifat administratif — tidak mempengaruhi stok. |
| `confirmed → preparing` | Dapur mulai mengerjakan order | Kitchen Staff | `assigned_to_id` di-set. Timer processing dimulai. |
| `preparing → ready` | Masakan selesai, siap dihidangkan | Kitchen Staff | Kitchen ticket sudah di-print. Status berubah ke "ready". |
| `ready → served` | Makanan dihidangkan ke meja (Dine In) / diserahkan ke kurir (Delivery) | Server / Fulfillment | Untuk Takeaway, biasanya langsung ke completed. |
| `served → completed` | Pelanggan konfirmasi terima / takeaway sudah diambil | Server / Kasir | Order masuk kolom Completed. Data masuk ke laporan. |
| `confirmed → cancelled` | Dapur membatalkan karena item habis | Kitchen Staff | Inventory adjustment otomatis untuk mengembalikan stok. |
| `preparing → cancelled` | Kendala saat memasak (bahan rusak, alat rusak) | Kitchen Staff / Manager | Stok yang sudah keluar dikembalikan via adjustment. |
| `ready → cancelled` | Pelanggan membatalkan sebelum makanan dihidangkan | Manager | Perlu approval manager karena order sudah di tahap akhir. |

***

## Sequence Diagram — Alur Order Placement → Kitchen → Fulfillment

### Alur 1: Order dari Kasir Offline (POS)

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Customer as Pelanggan
    participant Cashier as Kasir<br/>(CompanyPOSCashier)
    participant API as Server API
    participant DB as Database<br/>(CompanyPOSTransaction)
    participant BC as BroadcastChannel<br/>(snishop_order_queue)
    participant Kitchen as Dapur<br/>(POSOrderQueue)
    participant Printer as Thermal Printer<br/>(Bluetooth)
    participant Inventory as CompanyPOSInventory

    Customer->>Cashier: Pesan makanan (Dine In / Takeaway)
    Cashier->>Cashier: Input items, pilih payment method
    Cashier->>API: finalizeTransaction(items, payment, customer)
    API->>DB: INSERT CompanyPOSTransaction<br/>order_status = 'pending'
    DB-->>API: { _id, transaction_number, ... }
    API->>Inventory: INSERT log (type='out', qty per item)
    API->>BC: postMessage({ type: 'NEW_ORDER', order_id, ... })
    API-->>Cashier: Return transaction object

    BC->>Kitchen: Event diterima (< 100ms)
    Kitchen->>Kitchen: knownOrderIdsRef check → New order!
    Kitchen->>Kitchen: Browser Push Notification "Pesanan Baru!"
    Kitchen->>Kitchen: Sound alert 🔔
    Kitchen->>Printer: Auto-print Kitchen Ticket (jika enabled)
    Printer-->>Kitchen: Tiket tercetak (58mm/80mm)

    Note over Kitchen: Dapur mulai bekerja
    Kitchen->>API: transitionPOSOrderStatus(id, 'confirmed')
    API->>DB: UPDATE order_status = 'confirmed'
    API->>BC: postMessage({ type: 'STATUS_CHANGE', ... })

    Kitchen->>API: transitionPOSOrderStatus(id, 'preparing')
    API->>DB: UPDATE order_status = 'preparing',<br/>processed_at = NOW()
    API->>BC: postMessage({ type: 'STATUS_CHANGE', ... })

    Note over Kitchen: Masak selesai
    Kitchen->>API: transitionPOSOrderStatus(id, 'ready')
    API->>DB: UPDATE order_status = 'ready'
    API->>BC: postMessage({ type: 'STATUS_CHANGE', ... })

    Kitchen->>API: transitionPOSOrderStatus(id, 'served')
    API->>DB: UPDATE order_status = 'served'
    API->>BC: postMessage({ type: 'STATUS_CHANGE', ... })

    Kitchen->>API: transitionPOSOrderStatus(id, 'completed')
    API->>DB: UPDATE order_status = 'completed'
    API->>BC: postMessage({ type: 'STATUS_CHANGE', ... })
    Note over Kitchen: Order pindah ke kolom Completed 🟢
```

### Alur 2: Order dari Channel Online (QuinnOrder → CompanyPOSTransaction)

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Web as Quinn Website<br/>(Storefront)
    participant Checkout as Checkout API
    participant API as Server API
    participant QO as QuinnOrder
    participant CPT as CompanyPOSTransaction
    participant BC as BroadcastChannel<br/>(snishop_order_queue)
    participant Kitchen as Dapur<br/>(POSOrderQueue)

    Web->>Checkout: Customer submit order (items, address, payment_method)
    Checkout->>QO: INSERT QuinnOrder<br/>status='pending', payment_status='unpaid',<br/>fulfillment_status='unprocessed',<br/>reservation_status='active'
    QO-->>Checkout: { _id, order_number }

    Checkout->>API: Mirror ke ERP (create POS Transaction)
    API->>CPT: INSERT CompanyPOSTransaction<br/>source='website', sales_channel='website',<br/>order_status='pending', payment_status='pending'
    API->>QO: UPDATE pos_transaction_id = CPT._id

    Note over API: Pembayaran diverifikasi (manual transfer / COD)
    API->>QO: UPDATE payment_status='paid'
    API->>CPT: UPDATE payment_status='paid'
    API->>BC: postMessage({ type: 'NEW_ORDER', ... })

    BC->>Kitchen: Event diterima
    Kitchen->>Kitchen: Order muncul di kolom Pending 🟡<br/>dengan badge "Website"

    Note over Kitchen: Alur selanjutnya sama dengan Alur 1<br/>(confirmed → preparing → ready → served → completed)
```

### Alur 3: Order Cancellation & Inventory Rollback

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Kitchen as Dapur / Kasir
    participant API as Server API
    participant CPT as CompanyPOSTransaction
    participant INV as CompanyPOSInventory
    participant BC as BroadcastChannel

    Kitchen->>API: transitionPOSOrderStatus(id, 'cancelled')
    API->>API: Validasi: apakah stok sudah keluar?

    alt order_status = 'pending' (belum diproses)
        API->>CPT: UPDATE order_status = 'cancelled',<br/>cancelled_at = NOW()
        Note over API: Stok belum keluar — tidak perlu adjustment
    else order_status = 'confirmed' | 'preparing' (stok sudah keluar)
        API->>CPT: UPDATE order_status = 'cancelled',<br/>cancelled_at = NOW()
        API->>INV: INSERT log per item (type='in', qty = original qty)<br/>reason='cancelled_order', reference_id=CPT._id
        Note over INV: Stok dikembalikan ke level sebelum order
    end

    API->>BC: postMessage({ type: 'STATUS_CHANGE',<br/>order_id, new_status: 'cancelled' })
    BC->>Kitchen: Order hilang dari Kanban board
    Kitchen->>Kitchen: Update UI — order masuk terminal state
```

***

## RBAC — Role-Based Access Control untuk Order Status Transition

Tabel berikut mendefinisikan **siapa yang boleh melakukan transisi status order** di dalam POS Order Queue. Permission ini ditegakkan di level Server Function `transitionPOSOrderStatus` dan juga divalidasi di sisi UI.

| Transisi Status | Kasir | Kitchen Staff | Server / Fulfillment | Manager | Admin | Owner |
| - | :-: | :-: | :-: | :-: | :-: | :-: |
| `[*] → pending` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `pending → confirmed` | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
| `pending → cancelled` | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
| `pending → voided` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `confirmed → preparing` | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `preparing → ready` | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `ready → served` | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| `served → completed` | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| `confirmed → cancelled` | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `preparing → cancelled` | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `ready → cancelled` | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| `served → cancelled` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `completed → voided` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |

### Catatan RBAC (Bahasa Indonesia)

1. **Kasir** hanya bisa mengelola order di tahap awal (pending, confirmed) dan menyelesaikan order (served → completed). Kasir **tidak bisa** membatalkan order yang sudah masuk dapur.
2. **Kitchen Staff** memiliki kendali penuh atas alur dapur: confirmed → preparing → ready. Mereka juga bisa membatalkan order jika item habis atau ada kendala masak.
3. **Server / Fulfillment** hanya bisa melakukan transisi ready → served. Mereka tidak bisa memulai atau membatalkan order.
4. **Manager** memiliki akses hampir penuh — bisa melakukan semua transisi kecuali void (kesalahan administratif).
5. **Admin dan Owner** memiliki akses penuh ke semua transisi, termasuk void (penghapusan administratif) yang memerlukan audit trail.
6. **Void** hanya bisa dilakukan oleh Admin atau Owner karena bersifat irreversible dari sisi audit — order yang di-void tidak bisa dikembalikan.

### RLS (Row-Level Security)

Seluruh entitas order menggunakan RLS policy yang sama:

```json theme={null}
{
  "$or": [
    {
      "$and": [
        { "data.company_id": "{{user.data.active_company_id}}" },
        { "data.company_id": { "$nin": [null, ""] } }
      ]
    },
    { "created_by_id": "{{user.id}}" },
    { "user_condition": { "role": "admin" } }
  ]
}
```

Artinya: user hanya bisa membaca, membuat, mengupdate, dan menghapus data yang属于 `active_company_id` mereka, atau data yang mereka buat sendiri, atau semua data jika role = admin.

***

## BroadcastChannel Sync — `snishop_order_queue`

### Gambaran Umum

`BroadcastChannel` API adalah mekanisme **real-time cross-tab communication** dalam browser yang sama. SNISHOP ERP menggunakan channel bernama **`snishop_order_queue`** untuk menyinkronkan perubahan order antar-tab browser yang terbuka secara simultan — misalnya, satu tab menampilkan Kanban board di dapur, tab lain di kasir, dan tab ketiga di manajer.

```mermaid theme={null}
graph LR
    subgraph Browser["Browser (Multi-Tab)"]
        T1["Tab 1<br/>Dapur — Kanban Board"]
        T2["Tab 2<br/>Kasir — POS Cashier"]
        T3["Tab 3<br/>Manager — Dashboard"]
    end

    subgraph Channel["BroadcastChannel: snishop_order_queue"]
        BC["Channel Instance"]
    end

    T1 <-->|postMessage / onmessage| BC
    T2 <-->|postMessage / onmessage| BC
    T3 <-->|postMessage / onmessage| BC
```

### Payload Message

Setiap pesan yang dikirim melalui `snishop_order_queue` memiliki struktur berikut:

```typescript theme={null}
interface OrderQueueMessage {
  type: 'NEW_ORDER' | 'STATUS_CHANGE' | 'ORDER_REMOVED' | 'REFRESH';
  order_id: string;             // CompanyPOSTransaction._id
  transaction_number?: string;  // TRX-YYYYMMDD-XXXXXX
  new_status?: string;          // Nilai order_status baru
  previous_status?: string;     // Nilai order_status sebelumnya
  timestamp: string;            // ISO 8601 datetime
  actor?: string;               // User ID yang melakukan perubahan
  channel?: string;             // Sales channel asal order
}
```

### Event Types

| Event Type | Kapan Dikirim | Efek di Penerima |
| - | - | - |
| `NEW_ORDER` | Order baru dibuat (dari kasir atau channel online) | Order baru muncul di kolom Pending dengan notifikasi + sound alert. |
| `STATUS_CHANGE` | `order_status` berubah (confirmed, preparing, ready, served, completed, cancelled) | Order berpindah kolom Kanban. Timer di-reset sesuai kolom baru. |
| `ORDER_REMOVED` | Order di-void atau dihapus dari sistem | Order hilang dari Kanban board. |
| `REFRESH` | Perintah refresh manual dari admin | Semua tab melakukan refetch data dari server. |

### Implementasi Teknis

```javascript theme={null}
// 1. Inisialisasi BroadcastChannel
const bc = new BroadcastChannel('snishop_order_queue');

// 2. Kirim pesan setelah transisi status berhasil
function broadcastOrderChange(orderId, newStatus, previousStatus, actor) {
  bc.postMessage({
    type: 'STATUS_CHANGE',
    order_id: orderId,
    new_status: newStatus,
    previous_status: previousStatus,
    timestamp: new Date().toISOString(),
    actor: actor,
  });
}

// 3. Terima pesan dari tab lain
bc.onmessage = (event) => {
  const { type, order_id, new_status } = event.data;

  if (type === 'NEW_ORDER') {
    // Trigger notifikasi & sound
    showBrowserNotification(order_id);
    playSoundAlert();
    // Refetch data
    refetchOrders();
  }

  if (type === 'STATUS_CHANGE') {
    // Update order di Kanban board
    moveOrderToColumn(order_id, new_status);
  }

  if (type === 'REFRESH') {
    refetchOrders();
  }
};
```

### Fallback & Lapisan Real-Time

BroadcastChannel **hanya bekerja antar-tab di browser yang sama**. Untuk sinkronisasi antar-device/antar-user, SNISHOP ERP menggunakan lapisan real-time tambahan:

| Lapisan | Scope | Latency | Keterangan |
| - | - | - | - |
| **BroadcastChannel** | Antar-tab, browser yang sama | \< 100ms | Untuk multi-monitor di satu outlet (dapur + kasir). |
| **WebSocket Subscribe** | Antar-device, server push | \< 500ms | Subscribe ke perubahan `CompanyPOSTransaction` dan `QuinnOrder`. |
| **Polling 60s** | Fallback terakhir | \~300ms + network | Hanya aktif saat tab visible (`document.visibilityState`). |

### Best Practices untuk Developer

1. **Selalu broadcast setelah mutation** — setiap kali `transitionPOSOrderStatus` dipanggil dan berhasil, kirim pesan ke BroadcastChannel.
2. **Jangan broadcast sebelum DB confirmed** — tunggu response sukses dari server sebelum broadcast, agar tab lain tidak mendapat data yang belum committed.
3. **Debounce refetch** — jika banyak pesan diterima dalam waktu singkat (misal 5 order masuk bersamaan), debounce refetch agar tidak terlalu banyak API call.
4. **Close channel on unmount** — panggil `bc.close()` saat komponen unmount untuk mencegah memory leak.
5. **Handle stale data** — jika order di tab A sudah completed tapi tab B masih menampilkan sebagai pending, WebSocket subscribe akan mengoreksi dalam \< 500ms.

***

## Ringkasan Arsitektur Order Queue

```mermaid theme={null}
graph TB
    subgraph CHANNELS["Channel Penjualan"]
        POS["Offline POS<br/>(CompanyPOSCashier)"]
        WEB["Quinn Website<br/>(QuinnOrder)"]
        GRAB["GrabFood"]
        MKT["Marketplace"]
        WA["WhatsApp"]
    end

    subgraph ERP["SNISHOP ERP — Order Queue"]
        CPT["CompanyPOSTransaction<br/>order_status: 8 nilai"]
        KANBAN["Kanban Board<br/>3 Kolom: Pending / Processing / Completed"]
        SF["Server Function<br/>listPOSOrderQueue<br/>transitionPOSOrderStatus"]
    end

    subgraph REALTIME["Real-Time Sync"]
        BC["BroadcastChannel<br/>snishop_order_queue"]
        WS["WebSocket Subscribe"]
        POLL["60s Polling<br/>(tab visible only)"]
    end

    subgraph KITCHEN["Dapur & Fulfillment"]
        PRINT["Kitchen Ticket<br/>(Bluetooth Thermal)"]
        NOTIFY["Browser Push<br/>+ Sound Alert"]
        TIMER["Timer per Order<br/>(MM:SS)"]
    end

    POS --> CPT
    WEB --> CPT
    GRAB --> CPT
    MKT --> CPT
    WA --> CPT

    CPT --> SF
    SF --> KANBAN
    BC --> KANBAN
    WS --> KANBAN
    POLL --> KANBAN

    KANBAN --> PRINT
    KANBAN --> NOTIFY
    KANBAN --> TIMER
```

### Penjelasan Arsitektur (Bahasa Indonesia)

Seluruh order dari **semua channel penjualan** (Offline POS, Website Quinn, GrabFood, Marketplace, WhatsApp) bermuara ke satu entitas `CompanyPOSTransaction`. Field `order_status` dengan 8 nilai (`pending`, `confirmed`, `preparing`, `ready`, `served`, `completed`, `cancelled`, `voided`) menjadi **sumber kebenaran** untuk posisi order di Kanban board.

**POSOrderQueue** (862 baris) adalah komponen React yang menampilkan Kanban board 3 kolom. Data di-fetch melalui 4 mekanisme secara bersamaan: Server Function untuk initial load, BroadcastChannel untuk sinkronisasi antar-tab (\< 100ms), WebSocket untuk server push antar-device (\< 500ms), dan polling 60 detik sebagai fallback terakhir.

Setiap kali order baru masuk, sistem mendeteksi melalui `knownOrderIdsRef` dan memicu **Browser Push Notification** serta **Sound Alert** agar dapur tidak melewatkan pesanan. Kitchen ticket dicetak otomatis via Bluetooth thermal printer (58mm/80mm) untuk setiap order baru.

Transisi status order dikontrol oleh **RBAC** yang ketat — kasir mengelola tahap awal, dapur mengelola tahap memasak, server mengelola tahap penghidangan, dan hanya Admin/Owner yang bisa melakukan void (penghapusan administratif). Setiap transisi dicatat dalam audit trail dan di-broadcast ke semua client yang terhubung.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.