> ## 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.

# Customers

<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: "Customers — Database Pelanggan"
description: "Manajemen database pelanggan dengan paginasi, pencarian, import multi-format, WhatsApp integration, dan detail profil di SNISHOP ERP."
----------------------------------------------------------------------------------------------------------------------------------------------------

# Customers — Database Pelanggan

<img src="https://mintcdn.com/quinnofspicy/e4f_upKhVWcjsUmM/docs/mintlify/screenshots/crm/customers.png?fit=max&auto=format&n=e4f_upKhVWcjsUmM&q=85&s=4adb40922901c31014cb2c2fdac867b1" alt="Customers" width="1920" height="1080" data-path="docs/mintlify/screenshots/crm/customers.png" />

Halaman Database Pelanggan mengelola seluruh data pelanggan perusahaan. Ditampilkan dalam tabel paginasi dengan pencarian, filter status, export CSV, import multi-format (VCF/CSV/XLSX), dan integrasi WhatsApp.

## Arsitektur

```mermaid theme={null}
flowchart TB
    subgraph LIST["CustomerAudienceList"]
        PAG[Paginasi<br/>25 per halaman]
        SRCH[Pencarian<br/>Nama/Email/Telepon]
        FLT[Filter Status<br/>all/customer/prospect/lead/inactive]
        DESK[Desktop View<br/>Tabel]
        MOB[Mobile View<br/>Card]
    end

    subgraph ACTIONS["Aksi per Pelanggan"]
        WA[WhatsApp<br/>Quick Send]
        EDIT[Edit<br/>Dialog]
        DEL[Delete<br/>Confirm]
        DET[Detail<br/>Profile]
    end

    subgraph TOOLBAR["Toolbar"]
        ADD[Tambah Pelanggan]
        EXP[Export CSV]
        IMP[Import<br/>VCF/CSV/XLSX]
        BLAST[Blast Message]
    end

    subgraph ENTITY["Customer Entity"]
        F1[Contact Info<br/>name, email, phone, whatsapp]
        F2[Business Detail<br/>company, customer_type, source]
        F3[Address & Notes<br/>address, notes]
        F4[Membership<br/>level, points, since]
        F5[Analytics<br/>lifetime_value, total_orders, last_purchase]
        F6[Contact Tracking<br/>last_contact_date, method, notes]
    end

    LIST --> ACTIONS
    TOOLBAR --> LIST
    ENTITY --> LIST
```

## Akses Halaman

URL: `/crm` → tab **Database**

## Entitas: Customer

| Field | Tipe | Deskripsi |
| - | - | - |
| `name` | string | Nama pelanggan |
| `email` | string | Email |
| `phone` | string | Nomor telepon |
| `whatsapp_number` | string | Nomor WhatsApp |
| `company` | string | Nama organisasi/perusahaan |
| `address` | string | Alamat |
| `customer_type` | enum | `individual` atau `business` |
| `status` | enum | `lead`, `prospect`, `customer`, `inactive` |
| `source` | string | Sumber lead |
| `notes` | string | Catatan |
| `membership_level_id` | string | ID tier membership |
| `membership_level_name` | string | Nama tier |
| `membership_since` | date | Tanggal menjadi member |
| `lifetime_value` | number | Total nilai transaksi (LTV) |
| `total_orders` | number | Total jumlah order |
| `last_purchase_date` | date | Tanggal pembelian terakhir |
| `membership_points` | number | Poin loyalty |
| `last_contact_date` | datetime | Terakhir dihubungi |
| `last_contact_method` | string | Metode kontak terakhir |
| `last_contact_notes` | string | Catatan kontak terakhir |
| `company_id` | string | ID perusahaan (multi-company) |
| `created_at` | datetime | Tanggal dibuat |

## Tampilan List

### Desktop View

Tabel dengan kolom:

| Kolom | Field | Keterangan |
| - | - | - |
| Pelanggan | Avatar + `name` | Gradient avatar dari nama |
| Kontak | `email`, `phone` | Email dan telepon |
| Perusahaan | `company` | Nama organisasi |
| Status | `status` | Badge warna |
| LTV | `lifetime_value` | Format Rupiah |
| Orders | `total_orders` | Jumlah transaksi |
| Terakhir Beli | `last_purchase_date` | Tanggal |
| Membership | `membership_level_name` | Tier badge |

### Mobile View

Card layout dengan informasi yang sama, dioptimasi untuk layar kecil.

### VIP Badge

Pelanggan dengan `lifetime_value > 1.000.000` mendapatkan badge VIP (ikon bintang) di samping nama.

### Paginasi

25 pelanggan per halaman dengan navigasi halaman di bagian bawah.

## Form Tambah/Edit Pelanggan

Dialog form dengan 3 section:

### Section 1: Contact Info

| Field | Tipe | Required |
| - | - | - |
| Nama | text | Ya |
| Email | email | Tidak |
| Telepon | tel | Tidak |
| WhatsApp | tel | Tidak |

### Section 2: Business Detail

| Field | Tipe | Opsi |
| - | - | - |
| Tipe Pelanggan | select | `individual`, `business` |
| Perusahaan | text | — |
| Status | select | `lead`, `prospect`, `customer`, `inactive` |
| Sumber | select | Sumber lead |

### Section 3: Address & Notes

| Field | Tipe |
| - | - |
| Alamat | textarea |
| Catatan | textarea |

## Export CSV

Seluruh daftar pelanggan (sesuai filter aktif) bisa diexport ke CSV:

| Kolom CSV | Field |
| - | - |
| Nama | `name` |
| Email | `email` |
| Telepon | `phone` |
| WhatsApp | `whatsapp_number` |
| Perusahaan | `company` |
| Status | `status` |
| LTV | `lifetime_value` |
| Total Order | `total_orders` |
| Membership | `membership_level_name` |

## Import Multi-Format

### CustomerImportModal — 4 Step Wizard

```mermaid theme={null}
flowchart LR
    S1[Step 1<br/>Upload File] --> S2[Step 2<br/>Preview]
    S2 --> S3[Step 3<br/>Importing]
    S3 --> S4[Step 4<br/>Done]
```

### Format yang Didukung

| Format | Parser | Auto-Detect Columns |
| - | - | - |
| **VCF** (vCard) | Custom VCF parser | name, phone |
| **CSV** | Custom CSV parser | name/nama, phone/telepon, email, company/perusahaan, address/alamat, notes/catatan |
| **XLSX/Excel** | SheetJS library | Sama dengan CSV |

### Kolom Auto-Detection

Sistem mendeteksi nama kolom secara otomatis berdasarkan kata kunci:

| Field | Kata Kunci yang Dikenali |
| - | - |
| Name | `name`, `nama` |
| Phone | `phone`, `telepon`, `no_hp`, `hp` |
| Email | `email`, `surel` |
| Company | `company`, `perusahaan`, `organisasi` |
| Address | `address`, `alamat` |
| Notes | `notes`, `catatan` |

### Hasil Import

Setiap record yang diimport menjadi `Customer` dengan:

```
company_id: active_company_id
name: [from file]
phone: [from file]
email: [from file]
whatsapp_number: [from phone]
company: [from file]
address: [from file]
notes: [from file]
status: 'customer'
customer_type: 'individual'
source: 'import'
```

### Template CSV

Tersedia template CSV sample untuk download agar user tahu format yang diharapkan.

## Customer Detail Profile

### CustomerDetailProfile — Full-Screen Dialog

Dialog detail dengan sidebar profil dan 3 inner tab:

```
┌──────────────────────────────────────────────────────┐
│  ┌──────────┐  TABS: [Overview] [Activity] [AI]     │
│  │  Avatar   │                                       │
│  │  Nama     │  ┌─────────────────────────────────┐  │
│  │  Status   │  │  Tab Content                     │  │
│  │  Tier     │  │                                  │  │
│  │           │  │  Overview: Contact info, journey │  │
│  │  Stats:   │  │  Activity: Loyalty stamps,       │  │
│  │  LTV      │  │           transaction timeline   │  │
│  │  Orders   │  │  AI Copilot: Generate WhatsApp   │  │
│  │  Since    │  │              message via LLM     │  │
│  └──────────┘  └─────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘
```

### Tab Overview

Menampilkan informasi kontak lengkap dan customer journey:

| Info | Field |
| - | - |
| Tipe | `customer_type` |
| Email | `email` |
| Telepon | `phone` |
| WhatsApp | `whatsapp_number` |
| Organisasi | `company` |
| Membership Since | `membership_since` |
| Source | `source` |
| Alamat | `address` |
| Catatan | `notes` |

### Tab Activity

| Section | Konten |
| - | - |
| **Loyalty Stamps** | Display stamp/point balance |
| **Transaction Timeline** | Last purchase date, membership join date, customer creation date |

### Tab AI Copilot

AI Copilot menggunakan `invokeLLM` untuk generate pesan WhatsApp yang dipersonalisasi:

1. AI membaca profil pelanggan (nama, LTV, membership, last purchase)
2. Generate pesan yang relevan (promo, ucapan terima kasih, follow-up)
3. Tombol **"Send to WhatsApp"** untuk langsung kirim

## Address Book

### CustomerAddressBook — Multi-Address Management

Setiap pelanggan bisa memiliki beberapa alamat:

| Field | Tipe | Deskripsi |
| - | - | - |
| `label` | enum | `Rumah`, `Kantor`, `Gudang`, `Lainnya` |
| `recipient_name` | string | Nama penerima |
| `recipient_phone` | string | Telepon penerima |
| `full_address` | string | Alamat lengkap |
| `city_district` | string | Kota/kecamatan |
| `postal_code` | string | Kode pos |
| `notes` | string | Catatan |
| `is_default` | boolean | Alamat default |

### Operasi

| Operasi | Deskripsi |
| - | - |
| Tambah | Tambah alamat baru |
| Edit | Ubah data alamat |
| Hapus | Hapus alamat |
| Set Default | Jadikan alamat utama |

## WhatsApp Quick Send

### WhatsAppQuickSend — Single Customer

Mengirim pesan WhatsApp ke satu pelanggan:

```mermaid theme={null}
sequenceDiagram
    participant User
    participant WA as WhatsAppQuickSend
    participant GW as WhatsApp Gateway
    participant DB as Customer Entity

    User->>WA: Tulis pesan
    WA->>GW: checkGatewayDeviceStatus()
    GW-->>WA: Connected
    WA->>GW: whatsappSendMessage(number, message)
    GW-->>WA: Sent
    WA->>DB: Update last_contact_date, method, notes
```

Setelah terkirim, Customer entity diupdate:

* `last_contact_date` → `new Date()`
* `last_contact_method` → "whatsapp"
* `last_contact_notes` → isi pesan

## Blast Message

### CustomerBlastMessage — Broadcast

Mengirim pesan broadcast ke banyak pelanggan:

1. Pilih pelanggan dengan checkbox
2. Tulis pesan atau generate via AI
3. AI menggunakan konteks: jumlah audiens, rata-rata LTV
4. Kirim via `whatsappSendMessage` ke setiap pelanggan
5. Cek `WhatsAppSession` untuk memastikan gateway terhubung

## Tips Penggunaan

* Gunakan import VCF untuk memindahkan kontak dari telepon ke CRM
* Filter status untuk fokus pada segmen pelanggan tertentu
* Export CSV secara berkala untuk backup data pelanggan
* Manfaatkan AI Copilot untuk membuat pesan WhatsApp yang personal
* Update alamat pelanggan secara berkala untuk pengiriman yang akurat
* Monitor LTV untuk identifikasi pelanggan VIP

***

## Entity Relationship Diagram

Berikut adalah diagram relasi antara entitas Customer dengan entitas terkait di SNISHOP ERP. Setiap pelanggan dapat memiliki banyak alamat (embedded array), satu keanggotaan membership, banyak baris ledger loyalty, banyak Purchase Order, dan terhubung ke session WhatsApp.

```mermaid theme={null}
erDiagram
    Customer ||--o{ CustomerAddress : "addresses (embedded)"
    Customer ||--o| CustomerMembership : "membership_level_id"
    Customer ||--o{ CustomerLoyaltyLedger : "customer_id"
    Customer ||--o{ CustomerPO : "customer_id"
    Customer ||--o{ WhatsAppSession : "user_id / company_id"

    Customer {
        string id PK
        string company_id FK
        string name
        string email
        string phone
        string whatsapp_number
        string customer_type
        string status
        number lifetime_value
        number membership_points
    }

    CustomerAddress {
        string id
        string label
        string recipient_name
        string recipient_phone
        string full_address
        string city_district
        string postal_code
        boolean is_default
    }

    CustomerMembership {
        string id PK
        string company_id FK
        string level_name
        string level_key
        string scheme_type
        number discount_percentage
        number points_multiplier
        number min_purchase
    }

    CustomerLoyaltyLedger {
        string id PK
        string company_id FK
        string customer_id FK
        string event_type
        string scheme_type
        number points_delta
        number stamps_delta
        string idempotency_key
    }

    CustomerPO {
        string id PK
        string company_id FK
        string customer_id FK
        string po_number
        string status
        number total_amount
    }

    WhatsAppSession {
        string id PK
        string user_id FK
        string company_id FK
        string session_id
        string phone_number
        string status
    }
```

### Penjelasan Relasi

| Relasi | Kardinalitas | Keterangan |
| - | - | - |
| Customer → CustomerAddress | 1 : N (embedded) | Alamat tersimpan sebagai array di dalam dokumen Customer. Setiap pelanggan bisa punya banyak alamat (Rumah, Kantor, Gudang, dll) dengan satu alamat default. |
| Customer → CustomerMembership | N : 1 | Banyak customer merujuk ke satu tier membership. Tier menentukan skema loyalty (points/stamp/spending/visits/hybrid), diskon, dan multiplier poin. |
| Customer → CustomerLoyaltyLedger | 1 : N | Setiap transaksi loyalty (earn, redeem, return\_reversal, tier\_upgrade, manual\_adjustment) dicatat sebagai satu baris ledger dengan snapshot saldo sebelum dan sesudah. |
| Customer → CustomerPO | 1 : N | Customer B2B dapat membuat banyak Purchase Order. Setiap PO memiliki siklus hidup dari draft hingga delivered atau cancelled. |
| Customer → WhatsAppSession | N : M | Session WhatsApp dimiliki per user/company, digunakan untuk mengirim pesan ke banyak customer. Status koneksi menentukan apakah pesan bisa dikirim. |

***

## Complete Entity Schema: Customer

Entitas Customer memiliki **45 field** yang terbagi dalam 8 kategori. Berikut adalah schema lengkap berdasarkan definisi entity JSONC.

### Kategori 1: Identity (Identitas Dasar)

Field-field inti yang mengidentifikasi pelanggan.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | **Ya** | — | ID perusahaan (multi-company support). Setiap customer terikat ke satu perusahaan. |
| `name` | string | **Ya** | — | Nama lengkap pelanggan atau nama kontak B2B. |
| `email` | string | Tidak | — | Alamat email pelanggan. |
| `phone` | string | Tidak | — | Nomor telepon utama. |
| `whatsapp_number` | string | Tidak | — | Nomor WhatsApp khusus untuk follow-up dan broadcast. Jika kosong, fallback ke `phone`. |
| `auth_user_id` | string | Tidak | — | ID dari Auth provider (Base44 Auth). Digunakan jika pelanggan juga memiliki akun login. |
| `birthday` | date | Tidak | — | Tanggal ulang tahun. Dimanfaatkan untuk birthday bonus poin dan pesan otomatis. |
| `tags` | string\[] | Tidak | — | Label/tag untuk segmentasi (misal: "vip", "reseller", "jakarta"). |

### Kategori 2: Status & Klasifikasi

Menentukan posisi pelanggan dalam pipeline CRM dan tipe bisnisnya.

| Field | Tipe | Enum/Options | Default | Deskripsi |
| - | - | - | - | - |
| `status` | enum | `lead`, `prospect`, `customer`, `inactive` | `lead` | Status lifecycle pelanggan. Lihat **State Diagram** di bawah. |
| `customer_type` | enum | `individual`, `business`, `retail`, `reseller`, `distributor`, `modern_market` | `individual` | Klasifikasi tipe pelanggan. B2B menggunakan tipe selain `individual`. |
| `source` | string | — | — | Sumber lead: `ads`, `referral`, `walk-in`, `import`, `website`, dll. |
| `is_active` | boolean | — | `true` | Status aktif khusus B2B. `false` berarti customer sedang di-nonaktifkan sementara. |

### Kategori 3: Financial (Keuangan & Analitik)

Field yang menghitung dan menyimpan metrik nilai pelanggan.

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `lifetime_value` | number | `0` | **LTV** — Total akumulasi nilai transaksi sepanjang hubungan bisnis. |
| `total_orders` | number | `0` | Jumlah total transaksi/order yang pernah dilakukan. |
| `average_order_value` | number | `0` | **AOV** — Rata-rata nilai per transaksi (`lifetime_value / total_orders`). |
| `last_purchase_date` | date | — | Tanggal transaksi terakhir. Digunakan untuk menghitung recency. |
| `credit_limit` | number | `0` | Plafon kredit B2B. Maksimal piutang yang boleh berjalan. |

### Kategori 4: Membership & Loyalty

Seluruh field terkait program keanggotaan dan loyalty.

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `membership_level_id` | string | — | ID tier membership (merujuk ke `CustomerMembership`). |
| `membership_level_name` | string | — | Nama tier yang di-display (Silver, Gold, Platinum, dll). |
| `membership_since` | date | — | Tanggal pelanggan resmi menjadi member. |
| `membership_points` | number | `0` | Saldo poin loyalty saat ini. Bisa digunakan untuk redeem reward. |
| `lifetime_points` | number | `0` | Total poin yang pernah diperoleh sepanjang waktu (tidak berkurang saat redeem). |
| `stamps` | number | `0` | Jumlah stamp saat ini (untuk skema loyalty berbasis stamp). |
| `has_negative_points` | boolean | `false` | Flag yang menandakan saldo poin negatif akibat void/return setelah poin terpakai. |
| `loyalty_deficit_points` | number | `0` | Besarnya defisit poin yang membutuhkan rekonsiliasi manual oleh admin. |
| `loyalty_review_notes` | string | — | Catatan review dari admin mengenai penanganan deficit. |
| `last_tier_upgrade_date` | datetime | — | Waktu terakhir kali tier berubah (upgrade/downgrade). |
| `last_tier_upgrade_reason` | string | — | Alasan perubahan tier (misal: "Mencapai threshold Gold"). |
| `last_tier_upgrade_by` | string | — | Email operator/admin yang melakukan perubahan tier. |

### Kategori 5: Contact Tracking (Pelacakan Kontak)

Mencatat interaksi terakhir dengan pelanggan untuk follow-up.

| Field | Tipe | Enum/Options | Deskripsi |
| - | - | - | - |
| `last_contact_date` | date | — | Tanggal terakhir pelanggan dihubungi (follow-up). |
| `last_contact_method` | enum | `phone`, `whatsapp`, `email`, `meeting` | Metode kontak yang terakhir digunakan. |
| `last_contact_notes` | string | — | Catatan/hasil dari kontak terakhir (misal: "Berminat order 50 botol"). |

### Kategori 6: Personal (Preferensi Personal)

| Field | Tipe | Deskripsi |
| - | - | - |
| `preferences` | object | Objek preferensi pelanggan dengan sub-field: |
| `preferences.preferred_contact` | enum | `phone`, `whatsapp`, `email` — saluran kontak pilihan. |
| `preferences.preferred_payment` | string | Metode pembayaran favorit. |
| `preferences.language` | string | Bahasa pilihan (default: `id` = Indonesia). |

### Kategori 7: B2B (Business-to-Business)

Field khusus untuk pelanggan korporat, retailer, dan distributor.

| Field | Tipe | Enum/Options | Default | Deskripsi |
| - | - | - | - | - |
| `company` | string | — | — | Nama organisasi/perusahaan pelanggan. |
| `address` | string | — | — | Alamat utama (single-line). |
| `billing_address` | string | — | — | Alamat penagihan resmi untuk invoice B2B. |
| `shipping_address` | string | — | — | Alamat pengiriman default B2B. |
| `parent_customer_id` | string | — | — | ID perusahaan induk (untuk outlet/cabang retailer). |
| `outlet_name` | string | — | — | Nama cabang/outlet pelanggan. |
| `tax_id` | string | — | — | NPWP atau identitas administrasi pajak. |
| `payment_terms` | enum | `cash`, `net_7`, `net_14`, `net_30`, `net_60`, `custom` | `net_30` | Ketentuan jatuh tempo pembayaran. |
| `payment_terms_days` | number | — | `30` | Jumlah hari jatuh tempo (untuk `custom` atau referensi). |
| `default_address_id` | string | — | — | ID alamat default dari buku alamat (address book). |
| `addresses` | array | — | — | Array objek alamat (lihat CustomerAddress schema di atas). |

### Kategori 8: Metadata

| Field | Tipe | Deskripsi |
| - | - | - |
| `notes` | string | Catatan umum mengenai pelanggan. |
| `created_at` | datetime | Timestamp otomatis saat record dibuat. |
| `updated_at` | datetime | Timestamp otomatis saat record terakhir diubah. |
| `created_by_id` | string | ID user yang membuat record. |
| `updated_by_id` | string | ID user yang terakhir mengubah record. |

***

## Customer Lifecycle State Diagram

Berikut adalah diagram state machine untuk lifecycle pelanggan di SNISHOP ERP. Setiap pelanggan dimulai sebagai `lead` dan bergerak melalui pipeline hingga menjadi `customer` aktif atau `inactive`.

```mermaid theme={null}
stateDiagram-v2
    [*] --> lead: Import / Input Manual / Lead Baru

    lead --> prospect: Dikualifikasi\n(ada minat, verifikasi kontak)
    lead --> inactive: Tidak respons / Dibatalkan

    prospect --> customer: Transaksi pertama\n(purchase order / POS sale)
    prospect --> inactive: Tidak jadi order / Timeout

    customer --> customer: Transaksi berulang\n(LTV bertambah, poin loyalty)
    customer --> inactive: Tidak aktif > 90 hari\n(tidak ada transaksi)

    inactive --> lead: Re-aktivasi\n(kontak ulang, kampanye)
    inactive --> customer: Order langsung\n(tanpa kualifikasi ulang)

    inactive --> [*]: Dihapus permanen
```

### Penjelasan Transisi State

| Dari | Ke | Trigger | Keterangan |
| - | - | - | - |
| `[*]` | `lead` | Import VCF/CSV/XLSX, input manual, atau integrasi lead | Setiap kontak baru masuk sebagai lead. Status default dari entity adalah `lead`. |
| `lead` | `prospect` | Verifikasi oleh sales/CRM | Lead yang sudah dikonfirmasi minatnya, kontak valid, dan ada potensi bisnis. |
| `lead` | `inactive` | Tidak respons setelah follow-up | Lead yang tidak bisa dihubungi atau tidak berminat setelah beberapa kali percobaan. |
| `prospect` | `customer` | Transaksi pertama | Saat prospect melakukan pembelian pertama (POS sale atau PO), status otomatis berubah menjadi `customer`. |
| `prospect` | `inactive` | Timeout / batal | Prospect yang tidak melakukan transaksi dalam periode tertentu. |
| `customer` | `customer` | Transaksi berulang | Setiap transaksi menambah `lifetime_value`, `total_orders`, dan potentially `membership_points`. |
| `customer` | `inactive` | Tidak aktif > 90 hari | Customer yang tidak bertransaksi dalam 90 hari (konfigurasi bisa disesuaikan) ditandai sebagai inactive. |
| `inactive` | `lead` | Re-aktivasi / kampanye win-back | Admin mengubah status kembali ke `lead` untuk dimasukkan ke pipeline follow-up ulang. |
| `inactive` | `customer` | Order langsung | Customer lama yang langsung order ulang tanpa melalui proses kualifikasi. |

### Filter Status di Dashboard

| Filter | Query | Warna Badge | Deskripsi |
| - | - | - | - |
| **Semua** | Tanpa filter status | — | Menampilkan seluruh pelanggan tanpa filter status. |
| **Customer** | `status = 'customer'` | Hijau | Pelanggan aktif yang sudah pernah bertransaksi. |
| **Prospect** | `status = 'prospect'` | Biru | Calon pelanggan yang sudah dikualifikasi. |
| **Lead** | `status = 'lead'` | Kuning | Kontak baru yang belum dikualifikasi. |
| **Inactive** | `status = 'inactive'` | Merah | Pelanggan yang sudah tidak aktif. |

### Dashboard Stats

| Stat | Sumber Data | Format | Keterangan |
| - | - | - | - |
| Total Pelanggan | `count(Customers)` | Angka | Jumlah total pelanggan sesuai filter aktif. |
| Customer Aktif | `count(status = 'customer')` | Angka | Pelanggan yang sudah pernah bertransaksi. |
| Lead Baru | `count(status = 'lead')` | Angka | Lead yang menunggu kualifikasi. |
| Prospect | `count(status = 'prospect')` | Angka | Prospect yang sedang di-follow up. |
| Inactive | `count(status = 'inactive')` | Angka | Pelanggan yang sudah tidak aktif. |
| Total LTV | `sum(lifetime_value)` | Rupiah | Akumulasi nilai transaksi seluruh pelanggan. |

***

## Import Flow: Detail & Fuzzy Column Matching

Proses import mendukung tiga format file dengan kemampuan **fuzzy column matching** — sistem mencocokkan nama kolom dari file yang diimport dengan field Customer menggunakan kata kunci sinonim (bahasa Indonesia dan Inggris).

```mermaid theme={null}
flowchart TB
    subgraph UPLOAD["Step 1: Upload File"]
        F1[VCF File<br/>Kontak dari telepon]
        F2[CSV File<br/>Export dari sistem lain]
        F3[XLSX File<br/>Spreadsheet Excel]
    end

    subgraph PARSE["Step 2: Parsing & Fuzzy Matching"]
        P1{Detect Format}
        P2[VCF Parser<br/>Extract: N, TEL, EMAIL]
        P3[CSV Parser<br/>Split by delimiter]
        P4[XLSX Parser<br/>SheetJS → rows]
        P5[Fuzzy Column Matcher<br/>Cocokkan header dengan<br/>sinonim ID/EN]
    end

    subgraph PREVIEW["Step 3: Preview & Validasi"]
        V1[Tampilkan preview<br/>10 baris pertama]
        V2[Mapping kolom<br/>yang terdeteksi]
        V3[Validasi data<br/>required fields]
    end

    subgraph IMPORT["Step 4: Import ke Database"]
        I1[Batch insert<br/>ke Customer entity]
        I2[Set default values:<br/>status=customer<br/>source=import<br/>company_id=active]
        I3[Update progress bar<br/>dan counter]
    end

    subgraph DONE["Step 5: Selesai"]
        D1[Tampilkan ringkasan:<br/>berhasil, gagal, duplikat]
        D2[Refresh list pelanggan]
    end

    UPLOAD --> PARSE
    F1 --> P1
    F2 --> P1
    F3 --> P1
    P1 --> P2
    P1 --> P3
    P1 --> P4
    P2 --> P5
    P3 --> P5
    P4 --> P5
    PARSE --> PREVIEW
    PREVIEW --> IMPORT
    IMPORT --> DONE
```

### Fuzzy Column Matching — Tabel Sinonim Lengkap

Sistem menggunakan fuzzy matching untuk mengenali nama kolom dari berbagai format file. Berikut adalah pemetaan lengkap:

| Field Customer | Kata Kunci yang Dikenali | Contoh Header yang Cocok |
| - | - | - |
| `name` | `name`, `nama`, `full_name`, `nama_lengkap`, `contact` | "Nama Lengkap", "Full Name", "Contact Name" |
| `phone` | `phone`, `telepon`, `no_hp`, `hp`, `telp`, `mobile`, `no_telp`, `phone_number` | "No. HP", "Telepon", "Mobile Number" |
| `email` | `email`, `surel`, `mail`, `e-mail` | "Surel", "Email Address", "E-mail" |
| `company` | `company`, `perusahaan`, `organisasi`, `business`, `usaha` | "Nama Perusahaan", "Organisasi" |
| `address` | `address`, `alamat`, `addr` | "Alamat Lengkap", "Address" |
| `notes` | `notes`, `catatan`, `note`, `remark`, `keterangan` | "Catatan", "Remarks", "Keterangan" |
| `whatsapp_number` | `whatsapp`, `wa`, `no_wa`, `wa_number` | "No WhatsApp", "WA" |
| `birthday` | `birthday`, `ulang_tahun`, `dob`, `tgl_lahir` | "Tanggal Lahir", "Birthday" |
| `status` | `status`, `state` | "Status", "State" |
| `source` | `source`, `sumber`, `origin`, `asal` | "Sumber Lead", "Origin" |

### Penanganan Duplikat

Saat import, sistem melakukan pengecekan duplikat berdasarkan:

1. **Nomor telepon** — jika sudah ada customer dengan `phone` yang sama, record di-skip.
2. **Email** — jika sudah ada customer dengan `email` yang sama, record di-skip.
3. Ringkasan hasil import menampilkan jumlah: **berhasil**, **gagal** (validasi), dan **duplikat** (di-skip).

***

## WhatsApp Integration: Sequence Diagram

Integrasi WhatsApp di SNISHOP ERP menggunakan gateway Baileys (WhatsApp Web API). Setiap session WhatsApp disimpan di entitas `WhatsAppSession` dan digunakan untuk mengirim pesan ke pelanggan.

### Single Message Flow

```mermaid theme={null}
sequenceDiagram
    actor User as Sales / Admin
    participant UI as CustomerList / DetailProfile
    participant WA as WhatsAppQuickSend
    participant GW as WhatsAppSession<br/>(Gateway)
    participant DB as Customer Entity
    participant LEDGER as CustomerLoyaltyLedger

    User->>UI: Klik tombol WhatsApp pada pelanggan
    UI->>WA: Open dialog dengan nomor & pesan

    WA->>GW: checkGatewayDeviceStatus(session_id)
    GW-->>WA: { status: "connected" }

    alt Gateway Connected
        WA->>GW: whatsappSendMessage(phone, message)
        GW-->>WA: { sent: true, timestamp }

        WA->>DB: Update Customer
        Note over DB: last_contact_date = now()<br/>last_contact_method = "whatsapp"<br/>last_contact_notes = message

        WA->>LEDGER: (Opsional) Log aktivitas
        WA-->>UI: Toast: "Pesan terkirim"
    else Gateway Disconnected
        GW-->>WA: { status: "disconnected" }
        WA-->>UI: Error: "Gateway tidak terhubung. Silakan scan QR ulang."
    end
```

### Broadcast / Blast Message Flow

```mermaid theme={null}
sequenceDiagram
    actor User as Admin
    participant UI as CustomerBlastMessage
    participant AI as AI Copilot<br/>(invokeLLM)
    participant GW as WhatsAppSession<br/>(Gateway)
    participant DB as Customer Entity

    User->>UI: Pilih pelanggan dengan checkbox
    User->>UI: Tulis pesan manual / minta AI generate

    alt Generate via AI
        UI->>AI: invokeLLM(context)
        Note over AI: Konteks: jumlah audiens,<br/>rata-rata LTV, nama perusahaan
        AI-->>UI: Pesan yang dipersonalisasi
    end

    User->>UI: Klik "Kirim Broadcast"

    loop Untuk setiap pelanggan terpilih
        UI->>GW: whatsappSendMessage(phone, message)
        GW-->>UI: { sent: true }
        UI->>DB: Update last_contact_date, method, notes
        Note over UI: Delay antar pesan<br/>(rate limiting)
    end

    UI-->>User: Ringkasan: X terkirim, Y gagal
```

### WhatsApp Session Lifecycle

```mermaid theme={null}
stateDiagram-v2
    [*] --> disconnected: Session baru dibuat

    disconnected --> connecting: User klik "Connect"<br/>Generate QR Code
    connecting --> connected: User scan QR<br/>Baileys authenticated
    connecting --> error: Timeout / QR expired
    connecting --> disconnected: User cancel

    connected --> connected: Pesan terkirim / diterima<br/>(session maintained)
    connected --> disconnected: Baileys logout / restart
    connected --> error: Connection lost / network error

    error --> connecting: Retry / Reconnect
    error --> disconnected: User disconnect manual

    disconnected --> [*]: Session dihapus
```

### Status Koneksi WhatsApp Session

| Status | Ikon | Deskripsi | Aksi yang Tersedia |
| - | - | - | - |
| `connected` | Hijau | Gateway aktif dan siap kirim pesan | Send message, blast, AI copilot |
| `connecting` | Kuning | Sedang proses koneksi / menunggu scan QR | Tunggu hingga connected |
| `disconnected` | Merah | Gateway tidak terhubung | Scan QR ulang untuk connect |
| `error` | Merah | Koneksi error (network/timeout) | Retry atau scan QR ulang |

***

## Schema Entitas Terkait

### CustomerMembership — Tier Membership

Menentukan level keanggotaan dan benefit yang didapat pelanggan pada setiap tier.

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `company_id` | string | — | ID perusahaan (required). |
| `level_name` | string | — | Nama display tier (misal: "Silver", "Gold", "Platinum"). |
| `level_key` | string | — | Key unik lowercase (misal: "silver", "gold"). |
| `icon` | string | — | Emoji ikon untuk tier (misal: "🥈", "🥇"). |
| `color` | string | — | Warna tema tier untuk UI. |
| `description` | string | — | Penjelasan syarat dan keuntungan tier (max 1000 char). |
| `discount_percentage` | number | `0` | Persentase diskon untuk member tier ini (0-100). |
| `points_multiplier` | number | `1` | Multiplier perolehan poin (2 = double points). |
| `min_purchase` | number | `0` | Minimum akumulasi pembelian untuk mencapai tier ini. |
| `benefits` | string\[] | — | Daftar benefit (misal: \["Gratis ongkir", "Birthday gift"]). |
| `priority_support` | boolean | `false` | Akses priority support via WhatsApp. |
| `free_delivery` | boolean | `false` | Gratis ongkir untuk semua order. |
| `birthday_bonus` | number | `0` | Bonus poin spesial di hari ulang tahun. |
| `scheme_type` | enum | `points` | Skema loyalty: `points`, `stamp`, `spending`, `visits`, `hybrid`. |
| `points_threshold` | number | `10000` | Nominal belanja per perolehan poin (misal Rp 10.000 = 1 poin). |
| `points_per_threshold` | number | `1` | Jumlah poin per threshold. |
| `stamps_required` | number | `10` | Stamp yang dibutuhkan untuk reward/naik level. |
| `stamp_per_transaction` | number | `1` | Stamp diperoleh per transaksi eligible. |
| `min_redemption_points` | number | `0` | Minimum poin untuk bisa redeem reward. |
| `reward_type` | enum | `none` | Jenis reward: `none`, `discount_percentage`, `discount_amount`, `free_product`. |
| `reward_value` | number | `0` | Nilai reward (persentase atau nominal). |
| `reward_product_id` | string | — | ID produk jika reward berupa produk gratis. |
| `reward_product_name` | string | — | Nama produk reward gratis. |
| `expiry_days` | number | — | Masa kedaluwarsa poin dalam hari (opsional). |
| `order` | number | `0` | Urutan level (angka lebih tinggi = lebih premium). |
| `is_active` | boolean | `true` | Status aktif tier. |

### CustomerLoyaltyLedger — Buku Besar Loyalty

Mencatat setiap mutasi poin/stamp pelanggan sebagai audit trail yang lengkap.

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `company_id` | string | — | ID perusahaan (required). |
| `customer_id` | string | — | ID pelanggan pemilik transaksi loyalty (required). |
| `customer_name` | string | — | Nama customer saat transaksi (denormalized). |
| `customer_phone` | string | — | Nomor telepon customer (denormalized). |
| `transaction_id` | string | — | ID transaksi POS / sale asal. |
| `transaction_number` | string | — | Nomor struk / receipt. |
| `idempotency_key` | string | — | Key unik untuk mencegah double-posting (required). |
| `event_type` | enum | — | Tipe event: `earn`, `redeem`, `return_reversal`, `tier_upgrade`, `manual_adjustment`. |
| `scheme_type` | enum | `points` | Skema: `points`, `stamp`, `spending`, `visits`, `hybrid`. |
| `points_delta` | number | `0` | Perubahan poin (+ = bertambah, - = berkurang). |
| `points_before` | number | `0` | Saldo poin sebelum event. |
| `points_after` | number | `0` | Saldo poin setelah event. |
| `stamps_delta` | number | `0` | Perubahan stamp. |
| `stamps_before` | number | `0` | Saldo stamp sebelum event. |
| `stamps_after` | number | `0` | Saldo stamp setelah event. |
| `eligible_amount` | number | `0` | Nilai transaksi yang eligible untuk perolehan poin. |
| `reason` | string | — | Alasan/keterangan mutasi. |
| `deficit_points` | number | `0` | Defisit poin jika terjadi reversal setelah poin terpakai. |
| `requires_manual_review` | boolean | `false` | Flag jika transaksi butuh review manual (saldo negatif). |
| `reward_details` | object | — | Detail reward saat redeem (tipe, nilai, produk, biaya poin/stamp). |
| `performed_by` | string | — | Email/ID operator yang memproses. |
| `actor_role` | string | — | Role aktor: `owner`, `admin`, `cashier`. |
| `created_at` | datetime | — | Timestamp event. |
| `finance_record_id` | string | — | ID FinancialRecord untuk COGS reward (saat redeem). |

### CustomerPO — Purchase Order B2B

Purchase Order yang dibuat oleh atau untuk pelanggan B2B.

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `company_id` | string | — | ID perusahaan (required). |
| `customer_id` | string | — | ID customer B2B (required). |
| `customer_name` | string | — | Nama kontak pemesan (denormalized). |
| `customer_company` | string | — | Nama badan usaha customer. |
| `customer_email` | string | — | Email kontak pemesan. |
| `customer_phone` | string | — | Telepon kontak pemesan. |
| `customer_address` | string | — | Alamat pengiriman. |
| `po_number` | string | — | Nomor PO unik per company & customer (required). |
| `po_date` | date | — | Tanggal PO diterbitkan (required). |
| `expected_delivery_date` | date | — | Target tanggal pengiriman. |
| `attachment_url` | string | — | URL dokumen PDF/file PO dari customer. |
| `items` | array | — | Daftar produk dipesan: `product_id`, `sku`, `product_name`, `variant`, `unit`, `quantity`, `unit_price`, `subtotal`, `delivered_quantity`. |
| `subtotal` | number | `0` | Total sebelum pajak dan diskon. |
| `tax_percentage` | number | `0` | Persentase pajak. |
| `tax_amount` | number | `0` | Nominal pajak. |
| `discount_amount` | number | `0` | Nominal diskon. |
| `total_amount` | number | `0` | Total akhir setelah pajak dan diskon. |
| `notes` | string | — | Catatan/instruksi khusus pesanan. |
| `status` | enum | `draft` | Status PO: `draft`, `confirmed`, `invoiced`, `partially_delivered`, `delivered`, `cancelled`. |
| `invoice_id` | string | — | ID Invoice yang di-generate dari PO ini. |
| `idempotency_key` | string | — | Key untuk mencegah duplikasi saat import. |

### WhatsAppSession — Sesi Koneksi WhatsApp

Menyimpan state koneksi WhatsApp per user/company menggunakan Baileys.

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `user_id` | string | — | ID user pemilik session (required). |
| `user_email` | string | — | Email user (required). |
| `company_id` | string | — | ID company (null untuk personal). |
| `session_id` | string | — | Unique session identifier. |
| `phone_number` | string | — | Nomor WhatsApp yang terkoneksi. |
| `description` | string | — | Catatan mengenai sesi ini (max 1000 char). |
| `status` | enum | `disconnected` | Status: `disconnected`, `connecting`, `connected`, `error`. |
| `qr_code` | string | — | QR Code untuk scan (base64 atau URL). |
| `last_connected` | datetime | — | Terakhir kali berhasil connect. |
| `total_contacts` | number | `0` | Jumlah kontak yang pernah dihubungi. |
| `total_messages_sent` | number | `0` | Total pesan terkirim melalui session ini. |
| `session_data` | object | — | Data session Baileys (encrypted). |


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