# Impor & Deduplikasi Kontak


OmniStream menyediakan wizard impor CSV bertahap dan alat deduplikasi untuk menjaga direktori kontak tetap bersih. Semua fitur ini memerlukan izin `contacts.manage` (peran **Admin**).

## Impor Kontak via CSV

### Rute frontend

`/contacts/import-wizard`

### Langkah-langkah wizard

Wizard terdiri dari empat tahap berurutan:

| Tahap | Label | Keterangan |
|-------|-------|------------|
| 1 | **Upload** | Pilih file `.csv` dari perangkat Anda |
| 2 | **Map Columns** | Petakan setiap kolom CSV ke field kontak |
| 3 | **Preview** | Tinjau 5 baris pertama sebelum mengeksekusi |
| 4 | **Done** | Ringkasan hasil impor |

#### Tahap 1 — Upload

Klik **Choose File** dan pilih file CSV (format `text/csv`). File langsung dibaca di browser (tidak diunggah ke server dulu). Jika file tidak dapat di-parse, pesan error ditampilkan di tahap ini.

#### Tahap 2 — Pemetaan Kolom

Backend memanggil `POST /api/contacts/import/preview` dengan isi CSV mentah. Response berisi:

- `headers` — nama kolom dari baris pertama CSV
- `sample_rows` — hingga 5 baris data
- `total_rows` — jumlah baris data keseluruhan

Frontend otomatis menebak pemetaan berdasarkan nama kolom:

| Nama kolom CSV (case-insensitive) | Field tujuan |
|-----------------------------------|--------------|
| mengandung `name` | `name` |
| mengandung `phone` atau `mobile` | `phone_number` |
| mengandung `email` atau `mail` | `email` |
| mengandung `channel` | `channel` |
| mengandung `tag` | `tags` |
| lainnya | `— skip —` (diabaikan) |

Anda dapat mengubah pemetaan secara manual via dropdown sebelum melanjutkan. Kolom yang dipetakan ke `— skip —` tidak akan diimpor.

#### Tahap 3 — Preview

Menampilkan tabel sampel data beserta label field tujuan di bawah setiap header. Klik **Import N Contacts** untuk mengeksekusi.

Frontend mengirim `POST /api/contacts/import/execute` dengan body:

```json
{
  "csv_content": "<isi CSV lengkap>",
  "column_mapping": {
    "Nama Pelanggan": "name",
    "No HP": "phone_number",
    "Alamat Email": "email"
  }
}
```

Backend menggunakan `ON CONFLICT (phone_number) DO UPDATE` — kontak yang sudah ada berdasarkan `phone_number` akan diperbarui (nama dan email diisi jika kosong), bukan digandakan. Baris tanpa `phone_number` **dan** `email` dilewati (`skipped`).

:::note
`channel_source` untuk kontak yang diimpor via wizard di-set ke `'import'` secara otomatis. Jika kolom `channel` dipetakan, nilainya digunakan untuk impor via endpoint multipart (`POST /api/contacts/import`), bukan via wizard JSON.
:::

#### Tahap 4 — Hasil

| Metrik | Keterangan |
|--------|------------|
| **Imported** | Baris yang berhasil dimasukkan atau diperbarui |
| **Updated** | Ditampilkan di UI (dari field `updated` response) |
| **Errors** | Jumlah baris yang gagal |

Setelah selesai, klik **Import More** untuk memulai ulang atau **View Contacts** untuk kembali ke direktori kontak.

### Format CSV yang didukung

```csv
name,phone_number,email,channel
Budi Santoso,+628111222333,budi@example.com,whatsapp
Sari Dewi,,sari@example.com,email
Ahmad Fauzi,+628555666777,,
```

:::tip
Kolom `name`, `phone_number`, `email`, dan `channel` adalah yang paling berguna. Minimal harus ada salah satu dari `phone_number` atau `email` di setiap baris — baris yang tidak memiliki keduanya dilewati tanpa error.
:::

### Riwayat pekerjaan impor

Endpoint `GET /api/import-jobs` mengembalikan hingga 100 entri riwayat impor terbaru, diurutkan dari yang terbaru. Setiap entri mencakup:

| Field | Keterangan |
|-------|------------|
| `id` | UUID pekerjaan |
| `agent_id` | UUID agen yang menjalankan impor |
| `file_name` | Nama file CSV |
| `total_rows` | Total baris dalam file |
| `imported_rows` | Baris yang berhasil diimpor |
| `failed_rows` | Baris yang gagal |
| `status` | Status pekerjaan |
| `error_details` | Detail error (JSON, opsional) |
| `created_at` | Waktu mulai |
| `completed_at` | Waktu selesai (null jika masih berjalan) |

---

## Deteksi Duplikat

### Rute frontend

`/contacts/duplicates`

Halaman ini menampilkan grup kontak yang berbagi **nomor telepon** atau **alamat email** yang sama. Memerlukan izin `contacts.view`.

### Cara kerja deteksi

Backend (`GET /api/contacts/duplicates`) menjalankan dua query terpisah:

1. **Duplikat telepon** — kontak yang `phone_number`-nya muncul lebih dari sekali (tidak null, tidak kosong).
2. **Duplikat email** — kontak yang `email`-nya muncul lebih dari sekali, **dikecualikan** jika kontak tersebut sudah masuk ke grup duplikat telepon.

Hasilnya dikelompokkan menjadi objek `DuplicateGroup`:

```json
{
  "field": "phone_number",
  "value": "+628111222333",
  "contacts": [
    {
      "id": "uuid-a",
      "name": "Budi S",
      "phone_number": "+628111222333",
      "email": null,
      "channel": "whatsapp",
      "created_at": "2026-01-10T08:00:00Z"
    },
    {
      "id": "uuid-b",
      "name": "Budi Santoso",
      "phone_number": "+628111222333",
      "email": "budi@example.com",
      "channel": "import",
      "created_at": "2026-03-15T10:30:00Z"
    }
  ]
}
```

### Menggabungkan semua kontak dalam satu grup

Klik tombol **Merge All** pada kartu grup. Frontend secara otomatis:

1. Mengurutkan kontak dalam grup berdasarkan `created_at` ascending — kontak **tertua** menjadi primary.
2. Memanggil `POST /api/contacts/{primary_id}/merge/{secondary_id}` untuk setiap kontak non-primary secara berurutan.

:::warning
Proses merge tidak dapat dibatalkan. Kontak secondary akan dihapus permanen setelah semua percakapannya dipindahkan ke primary.
:::

---

## Saran Merge

### Rute frontend

`/contacts/merge-suggestions`

Halaman ini menampilkan pasangan kontak yang diduga duplikat beserta **skor kepercayaan** (confidence). Memerlukan izin `contacts.manage`.

### Cara kerja saran

Backend (`GET /api/contacts/merge-suggestions`) menjalankan query serupa dengan deteksi duplikat, lalu menghasilkan pasangan `primary`–`secondary` di mana primary adalah kontak yang lebih lama (`created_at` lebih awal). Skor kepercayaan dihitung sebagai:

| Kondisi | Confidence |
|---------|------------|
| Nomor telepon sama **dan** nama sama | 0.95 (95%) |
| Nomor telepon sama, nama berbeda | 0.80 (80%) |
| Email sama **dan** nama sama | 0.95 (95%) |
| Email sama, nama berbeda | 0.80 (80%) |

Confidence ditampilkan sebagai badge berwarna:

| Rentang | Warna |
|---------|-------|
| ≥ 90% | Hijau |
| 70–89% | Amber |
| < 70% | Merah |

Setiap kartu menampilkan kontak **Primary** (latar hijau) dan **Secondary** (latar abu-abu). Secondary akan digabungkan ke dalam primary.

### Menggabungkan dari halaman saran

Klik tombol **Merge** pada kartu saran. Frontend memanggil:

```http
POST /api/contacts/{primary_id}/merge/{secondary_id}
Authorization: Bearer <jwt>
```

Setelah berhasil, kartu saran yang bersangkutan dihapus dari daftar tanpa reload penuh.

---

## Proses Merge Kontak

Endpoint `POST /api/contacts/{primary_id}/merge/{secondary_id}` menjalankan operasi berikut dalam satu transaksi database:

1. **Pindahkan percakapan** — semua percakapan milik secondary dipindahkan ke primary (`UPDATE conversations SET contact_id = primary_id`).
2. **Gabungkan tags** — array tags dari kedua kontak digabung dan dideduplikasi.
3. **Isi field kosong** — field `name`, `email`, dan `phone_number` pada primary diisi dari secondary jika primary kosong untuk field tersebut.
4. **Hapus secondary** — kontak secondary dihapus dari database.
5. **Catat activity log** — aksi `merge_contacts` dicatat di tabel `activity_logs` beserta metadata (nama/telepon primary & secondary, jumlah percakapan yang dipindah).

:::note
Merge tidak dapat dilakukan pada kontak yang sama (`primary_id == secondary_id`) — backend mengembalikan HTTP 400.
:::

### Contoh response sukses

```json
{
  "id": "uuid-primary",
  "name": "Budi Santoso",
  "phone_number": "+628111222333",
  "email": "budi@example.com",
  "channel_source": "whatsapp",
  "tags": ["vip", "pelanggan-lama"],
  "created_at": "2026-01-10T08:00:00Z",
  "updated_at": "2026-06-08T11:30:00Z"
}
```

---

## Riwayat Merge

### Per kontak

```http
GET /api/contacts/{id}/merge-history
Authorization: Bearer <jwt>
```

Mengembalikan semua entri merge di mana kontak ini pernah menjadi primary **atau** secondary.

### Semua merge (admin)

```http
GET /api/contacts/merge-history
Authorization: Bearer <jwt>
```

Mengembalikan hingga 200 entri merge terbaru di seluruh workspace.

### Struktur entri riwayat

| Field | Keterangan |
|-------|------------|
| `id` | UUID entri |
| `primary_contact_id` | UUID kontak yang menerima (bertahan) |
| `secondary_contact_id` | UUID kontak yang dihapus |
| `merged_by` | UUID agen yang melakukan merge |
| `merge_data` | JSON metadata: `secondary_name`, `secondary_phone`, `conversations_transferred`, dll. |
| `created_at` | Waktu merge dilakukan |

---

## Ringkasan Endpoint

| Aksi | Endpoint | Izin |
|------|----------|-------|
| Preview CSV | `POST /api/contacts/import/preview` | `contacts.manage` |
| Eksekusi impor (wizard) | `POST /api/contacts/import/execute` | `contacts.manage` |
| Impor via multipart | `POST /api/contacts/import` | `contacts.manage` |
| Riwayat pekerjaan impor | `GET /api/import-jobs` | Login |
| Deteksi duplikat | `GET /api/contacts/duplicates` | `contacts.view` |
| Saran merge | `GET /api/contacts/merge-suggestions` | `contacts.manage` |
| Gabungkan kontak | `POST /api/contacts/{primary_id}/merge/{secondary_id}` | `contacts.manage` |
| Riwayat merge (per kontak) | `GET /api/contacts/{id}/merge-history` | Login |
| Riwayat merge (semua) | `GET /api/contacts/merge-history` | Login |

Skema request dan response lengkap tersedia di [API Reference](/api).

## Lihat juga

- [Manajemen Kontak](/panduan/agent/kontak) — direktori kontak, pencarian, dan detail kontak
- [Activity Logs](/panduan/admin/activity-logs) — log aksi `import_contacts` dan `merge_contacts`
