Lewati ke konten utama

Alur Chat Rooms (Agen Manusia)

Panel agen di /admin/chat-rooms. Dari sisi teknis, halaman ini adalah live proxy ke ChatPanel Jasnita — bukan pembaca database lokal.


Arsitektur: baca dari Jasnita, tulis ke Jasnita

LapisanTanggung jawabPerilaku saat error
ChatPanelServiceBaca daftar percakapan, thread, jumlah per tab, tandai dibaca; menempelkan pemegang & labelMenelan semua error → kembalikan kosong, panel tidak pernah fatal
ChatRoomServiceKirim teks / media / template, catat ke whatsapp_messages, klaim percakapanMelempar ke UI → notifikasi merah ke agen
ChatHandlerResolverMenyimpulkan siapa yang terakhir membalas: AI, agen, bot Jasnita, atau belum adaMenelan error → "belum dibalas"
ChatTakeoverServiceMenyimpan siapa yang memegang percakapan (contacts.handled_by)
ChatLabelServiceLabel percakapan (global untuk seluruh tim)
ToschatClientOAuth2, HTTP, retry 401, endpointMelempar RuntimeException dengan potongan body

Pemuatan halaman: sengaja ditunda

Kenapa ditunda?

Kalau data ditarik saat render pertama, halaman memblokir menunggu balasan API Jasnita — agen melihat layar putih beberapa detik. Sekarang kerangka halaman muncul instan, data menyusul.

Cache: daftar percakapan & thread 5 detik, jumlah per tab 20 detik. Ini mencegah puluhan agen yang polling membanjiri API Jasnita.


Membaca percakapan

Tiga tab

Tab UIStatus Jasnita
Queuequeue
Assignedassigned
Solvedsolved

Ganti tab akan mereset percakapan terpilih, thread, halaman, dan panel detail — lalu memuat ulang.

Bentuk data yang dinormalkan

Respons ChatPanel diubah jadi bentuk siap-render:

FieldSumberCatatan
idconversation_id
namecontact.name, fallback ke nomor
numbercontact.contact_id
last_messagemessage, fallback 📎 Media
last_timecreated_atSudah WIB dari Jasnita — tidak digeser lagi
unreadunread_count
respondedlast_chat_by > 0 atau is_read = '1'
handled_byChatTakeoverService → fallback ChatHandlerResolverLihat bagian di bawah
labelsChatLabelService::labelsFor()Satu query untuk seluruh halaman

Pesan dalam thread ditandai outbound: true kalau sender sama dengan gateway_account_id akun kita, lalu diurutkan berdasarkan created_at.


Siapa yang memegang percakapan

Dua lapisan berbeda, dan urutannya penting.

Kenapa serumit ini

Tiga fakta dari API Jasnita yang sudah diverifikasi langsung:

Tiga jebakan API Jasnita
  1. Daftar percakapan tidak membawa identitas pengirim sama sekali. last_chat_by hanya bernilai 0/1, dan 0 berarti "pelanggan yang bicara terakhir"bukan "belum pernah dibalas". Data nyata membuktikannya: ada percakapan berisi "baik kak terima kasih" dari pelanggan (jelas sudah dilayani agen) yang tetap bernilai 0. Karena itu identitas hanya bisa diambil dari get-detail.
  2. user_id tidak bisa memisahkan AI dari manusia. Kiriman lewat API kita tercatat dengan user_id akun API, dan agen manusia yang login ke panel Jasnita memakai akun API yang sama menghasilkan user_id yang persis sama.
  3. user_id = 0 pada pesan keluar adalah autoresponder Jasnita, bukan AI kita — AI kita selalu lewat API sehingga selalu punya user_id akun API.

Yang bisa dipercaya adalah id pesan: setiap pesan yang kita kirim tersimpan lokal beserta wa_msg_id-nya, jadi pesan keluar yang tidak punya padanan lokal pasti diketik orang di panel Jasnita.

Ketiadaan data dilaporkan apa adanya

Sebelumnya ketiadaan data diam-diam diklaim sebagai "AI" — itu yang membuat setiap percakapan tampak dipegang AI. Sekarang ia dilaporkan sebagai "belum dibalas".

Cache per id pesan terakhir

resolve() harus memanggil get-detail untuk setiap baris percakapan — mahal kalau dilakukan tiap polling. Karena itu hasilnya di-cache dengan kunci conversation_id + id pesan terakhir (TTL 1 jam):


Ambil alih percakapan (takeover)

SifatKeterangan
OtomatisMengirim pesan manual dari Chat Rooms langsung mengklaim percakapan — tidak ada langkah tambahan yang harus diingat
Menimpa semuaTakeover eksplisit memblokir auto-reply terlepas dari toggle "Stop AI When Human Chat"
Lokal sajaTidak disinkronkan ke Jasnita. Tab Queue/Assigned/Solved tetap milik Jasnita; lapisan takeover berdiri sendiri
Tidak ada baris = dipegang AIKontak yang belum pernah ada tidak perlu dibuat saat melepas ke AI

Label percakapan

Label buatan pengguna sendiri (mis. "sudah dibalas" / "belum dibalas"), global untuk seluruh tim.

Keputusan desainAlasan
Menempel ke contacts.contact_id, bukan conversation_id JasnitaNomor kontak unik dan stabil; conversation_id bisa berganti antar sesi sehingga label akan "hilang"
Nama label unik (case-insensitive)Dua agen yang mengetik "Sudah dibalas" harus berakhir di label yang sama, bukan dua label kembar yang membuat filter setengah-setengah
Palet warna dibatasi 7 pilihanTampilan daftar percakapan tetap konsisten
Kelas warna didefinisikan di modelDaftar percakapan, header thread, dan pemilih label memakai palet yang persis sama

Label dikelola lewat resource Chat Labels (/admin/chat-labels), dan bisa dipasang/dilepas langsung dari panel Chat Rooms.

Pencarian

Pencarian bersifat client-side atas daftar yang sudah dimuat — tidak memanggil API. Cocokkan pada nama (case-insensitive) atau nomor. Tombol "muat lebih banyak" menambah halaman dan memuat ulang halaman 1..N.


Menandai sudah dibaca

Saat agen memilih percakapan, sistem otomatis memanggil mark-as-read-by-conversation-id ke Jasnita, lalu menyegarkan daftar & jumlah. Kegagalannya hanya dilog — tidak mengganggu agen.


Mengirim pesan

Jendela 24 jam WhatsApp

Meta hanya mengizinkan pesan bebas kalau pelanggan mengirim pesan dalam 24 jam terakhir. Di luar jendela itu Meta membuang pesan sementara Toschat tetap membalas status "pending" — jadi sistem tidak bisa tahu pesannya gagal. Untuk memulai percakapan setelah lewat 24 jam, satu-satunya cara sah adalah template.

Semua pesan keluar dicatat lokal

JalurMetodesent_by
Balas cepat di threadsendTextToNumber / sendMediaToNumberhuman + sent_by_user_id
Template & media ke ContactsendTemplate / sendMediahuman + sent_by_user_id
Balasan otomatis AIWhatsappAutoReplyServiceai
Notifikasi pembayaranBookingConfirmationNotifier(kosong — diperlakukan sebagai bot)
Balasan agen WAJIB dicatat

Panel membaca tampilannya dari Jasnita, jadi mencatat lokal terasa mubazir. Tapi whatsapp_messages adalah satu-satunya riwayat yang dilihat AI. Tanpa catatan ini AI tidak pernah tahu ada agen human yang menjawab, lalu ikut membalas di percakapan yang sudah diambil alih.

Nomornya juga harus disimpan dalam bentuk ternormalisasi (08…62…), karena itulah bentuk yang dicari WhatsappAutoReplyService::loadHistory(). Bentuk lain tidak akan terbaca.


Media: kenapa lewat proksi

Bearer token tidak pernah menyentuh browser

Berkas media di Jasnita hanya bisa diunduh dengan Bearer token. Kalau <img src> menunjuk langsung ke Jasnita, token harus ditempel di URL dan bocor ke riwayat browser. Proksi ini menjaga token tetap server-side, sekaligus memaksa pemeriksaan izin per permintaan.


Buat pembayaran booking manual

Tombol di panel detail. Berbeda dengan booking lewat AI: tidak membawa jadwal.

Karena tidak ada BookingRequest, saat callback lunas nanti listener akan berakhir di outcome none → pasien menerima pesan "Pembayaran reservasi Anda telah kami terima" tanpa jadwal, dan Log::warning "Pembayaran lunas tanpa draft booking" ditulis.

Outcome none punya dua arti

Selain reservasi manual yang wajar, none juga tanda tangan jendela sempit "invoice Duitku berhasil, insert draft gagal" — yaitu uang masuk tanpa booking. Keduanya tidak bisa dibedakan dari listener, jadi semuanya dilog supaya ops bisa memeriksanya.


Autentikasi ke Jasnita

Token berlaku 24 jam. Kredensial disimpan per akun WhatsApp dan terenkripsi di database (gateway_client_secret, gateway_password).

Endpoint Toschat yang dipakai

EndpointFungsi
POST /api/v1/oauth/tokenAmbil access token
POST /api/v1/chat/sendKirim teks / media / template
GET /api/v1/message-template/getDaftar template
POST /api/v1/chat/mark-as-readTandai satu pesan dibaca
POST /api/v1/chat/mark-as-read-by-conversation-idTandai satu percakapan dibaca
GET /api/v1/file?p=Unduh media
POST /api/v1/webhook/updateDaftarkan URL webhook
GET /api/v1/subscriptionsDaftar channel aktif (uji koneksi)
GET /api/v1/contactDaftar kontak (paginasi)
GET /api/v1/chatpanel/get-with-filter/{status}/...Daftar percakapan per status
POST /api/v1/chatpanel/get-detailIsi thread percakapan
GET /api/v1/chatpanel/get-status-count-with-status-filter/...Jumlah per status
Endpoint yang tidak terdokumentasi publik

Endpoint chatpanel/* dan GET /api/v1/contact adalah endpoint internal ChatPanel yang tidak ada di koleksi Postman publik Toschat, tetapi bisa dipanggil dengan token OAuth kita. Folder "Contacts" yang terdokumentasi hanya menyediakan active-conversation per nomor, sehingga tidak bisa dipakai sinkronisasi massal.


Kontrol akses

Halaman Chat Rooms dijaga permission Shield View:ChatRoomsPage. super_admin selalu punya akses. Detail di Hak Akses.


Prasyarat agar Chat Rooms berfungsi

Kalau salah satu tidak terpenuhi, gejalanya ada di tabel troubleshooting Alur Harian Agen Chat.