Konco punya sistem tiket bantuan terpadu dengan live chat real-time antara member dan admin. Realtime diimplementasi via Laravel Reverb (WebSocket) dengan broadcast events. Selain itu, widget Crisp Chat terintegrasi di layout member portal untuk live support instan.
Arsitektur realtime
text Copy Member browser Admin browser
│ │
│ Laravel Echo + Pusher JS │ Laravel Echo + Pusher JS
│ (connect to Reverb) │ (connect to Reverb)
▼ ▼
┌───────────────────────────────────────────────────────────────┐
│ Laravel Reverb WebSocket (port 8080) │
│ │
│ Channels: │
│ private-ticket.{ticket_id} ← per-tiket chat room │
│ private-admin-request.{admin_id} ← admin request notif │
└─────────────┬─────────────────────────────────┬───────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────────┐
│ dazo-whitelist (web app) │ │ Admin dashboard │
│ │ │ │
│ TicketController │ │ TicketController (admin) │
│ → TicketMessage::create(...) │ │ → listen channel │
│ → event(new TicketMessage │ │ → update UI │
│ Created($message)) │ │ │
│ │ │ Notification::send(...) │
│ Notification (database + │ │ → broadcast │
│ ShouldBroadcast) │ │ │
└─────────────────────────────────┘ └─────────────────────────────┘
Laravel Reverb
Aspek Keterangan Package laravel/reverb ^1.0Server php artisan reverb:start (di-start local:start)Port REVERB_PORT=8080 (default)Protocol WebSocket, kompatibel Pusher JS Driver BROADCAST_DRIVER=reverbFrontend Laravel Echo + pusher-js — config VITE_REVERB_* Config config/reverb.php
Event yang di-broadcast
Event Trigger Channel Payload TicketMessageCreatedPesan tiket dikirim private-ticket.{ticket_id}TicketMessage instanceTicketStatusUpdatedStatus tiket berubah private-ticket.{ticket_id}Ticket old/new status AdminRequestCreatedAdmin request baru private-admin-request.{admin_id}AdminRequest instanceAdminSubscriptionReminderNotificationOrder paid belum diproses Private admin channel Order instanceAdminWithdrawReminderNotificationWithdraw belum selesai Private admin channel Order instance
Events berlokasi di app/Events/.
Channel authorization
Broadcast channel authorization di routes/channels.php:
php Copy // private-ticket.{ticket_id}
Broadcast :: channel ( ' ticket.{id} ' , function ($ user , $ id ) & #123;
$ticket = Ticket::find ($ id ) ;
// Hanya member pemilik atau admin yang dapat akses
return $ticket & & ( $ticket->member_id === $user->member->id ?? null
|| $user->guard_name === 'admin' ) ;
& #125;);
// private-admin-request.{admin_id}
Broadcast::channel ( 'admin-request.& #123;id}', function ($user, $id) {
return $ user ->id === $ id || $ user -> guard_name === ' admin ' ;
& #125;);
Admin broadcasting auth Route khusus untuk admin broadcast auth: POST /admin/broadcasting/auth (routes/web.php:123) — middleware web, auth:admin.
Ticket model lifecycle
Ticket model (app/Models/Ticket.php) menggunakan booted() hooks untuk side effects:
creating — auto-set admin_id null
php Copy static:: creating ( function ( Ticket $ e ) & #123;
// Do not auto-assign admin on create.
// admin_id should be set explicitly when a ticket is assigned.
if ( !$e->admin_id ) & #123;
$e->admin_id = null;
& #125;
& #125;);
created — log activity
php Copy static:: created ( function ( Ticket $ e ) & #123;
$ticketNumber = str_pad ( (string ) $e->ticket_number, 4, '0', STR_PAD_LEFT);
$e->activities () ->create ( [
'user_id' => auth( ) ->id () ,
'action' => 'created',
'message' => "Tiket #{$ticketNumber} telah dibuat",
]);
& #125;);
updated — notification side effects
Trigger Notification Email admin_id berubah & tidak nullTicketHandledNotification to member + AdminTicketAssignedNotification to adminemail.ticket.ongoingstatus berubah ke closedTicketClosedNotification to memberemail.ticket.closedstatus berubah ke open & reopened_at setTicketReopenedNotification to memberemail.ticket.reopened
deleting — cascade
php Copy static:: deleting ( function ( Ticket $ e ) & #123;
if ( auth( ) ->user () ->guard_name != 'admin' && $e->status != 'pending') return false;
foreach ( $e->message as $ val ) $val->delete () ;
foreach ( $e->ads_account as $ val ) $val->delete () ;
& #125;);
Ticket status lifecycle
text Copy [pending] ──admin assign──► [open/on progress] ──admin close──► [closed]
▲ │
│ │
└──reopen─────────────────────────┘
Status Arti Badge color pendingTiket baru, belum ditugaskan admin blue openSudah ditugaskan, dalam progress info/warning on progressSedang dikerjakan admin warning closedSelesai/ditutup red
Status derivasi Ticket::getStatusInfoAttribute() meng-derive on progress bila status='open' & admin_id terisi. Jadi status aktual bisa open tapi ditampilkan on progress.
Ticket auto-assignment
php Copy static function getadmin ( ) & #123;
$ cs = Config :: get_val ( ' ticket_support_role ' );
if ( ! Str :: isUuid ($ cs )) return ;
return User :: whereHas ( " roles " , fn ($ q ) => $ q -> where ( " id " , $ cs ))
-> admin () -> active () -> inRandomOrder () -> pluck ( ' id ' ) -> first ();
& #125;
Auto-assign admin bila config ticket_support_role (UUID role) diset. Admin dipilih random dari user dengan role tersebut.
Crisp Chat (dual-channel)
Selain WebSocket internal, Konco terintegrasi Crisp Chat untuk live support instan di member portal.
Konfigurasi
Config Env Keterangan Website ID CRISP_WEBSITE_IDID dari Crisp dashboard Enabled CRISP_ENABLEDToggle on/off Locale hardcoded "id" Bahasa Indonesia Theme color hardcoded #2563eb Match project theme Nickname hardcoded “Support Team” Default team name
Z-index hierarchy (dual-channel dengan WhatsApp)
Element Z-Index Posisi Crisp widget 1000Pojok kanan bawah WhatsApp button 999Digeser kiri Crisp Other CTA 998—
css Copy /* WhatsApp button digeser ke kiri Crisp */
. wa-float-dual & # 123 ;
right: 95px !important;
bottom: 20px !important;
z-index : 999;
& # 125 ;
. crisp-client & # 123 ;
z-index : 1000 !important;
& # 125 ;
User identity sync
Saat member login, Crisp otomatis dapat identitas:
javascript Copy window . CRISP_READY_TRIGGER = function () { ;
$crisp . push ([ " set " , " user:email " , [ " {{ auth()->user()->email }} " ]]);
$crisp . push ([ " set " , " user:nickname " , [ " {{ auth()->user()->name }} " ]]);
& # 125 ;;
Notification channels
Notification Channels Penerima TicketCreatedNotificationdatabase + broadcast Admin TicketHandledNotificationdatabase + broadcast + mail Member TicketClosedNotificationdatabase + broadcast + mail Member TicketReopenedNotificationdatabase + broadcast + mail Member AdminTicketNewNotificationdatabase + broadcast Admin AdminTicketAssignedNotificationdatabase + broadcast Admin
Notification model dual-lapis Ticket events trigger Notification::send() → notifikasi disimpan di tabel notifications (database channel) + di-broadcast ke WebSocket (broadcast channel). Frontend dengar kedua: UI update via WebSocket, history via API.
Frontend implementation
Echo initialization
javascript Copy // resources/js/app.js (disederhanakan)
import Echo from ' laravel-echo ' ;
import Pusher from ' pusher-js ' ;
window . Echo = new Echo ( & # 123 ;
broadcaster : ' pusher ' ,
key : import . meta . env . VITE_REVERB_APP_KEY ,
wsHost : import . meta . env . VITE_REVERB_HOST ,
wsPort : import . meta . env . VITE_REVERB_PORT ,
wssPort : import . meta . env . VITE_REVERB_PORT ,
forceTLS : false ,
enabledTransports : [ ' ws ' , ' wss ' ],
authEndpoint : ' /broadcasting/auth ' ,
& # 125 ; );
Subscribe ticket channel
javascript Copy Echo . private ( ` ticket.${ticketId} ` )
. listen ( ' TicketMessageCreated ' , ( e ) => & # 123 ;
// Append message to chat UI
& # 125 ; )
. listen ( ' TicketStatusUpdated ' , ( e ) => & # 123 ;
// Update ticket status badge
& # 125 ; );
Troubleshooting realtime
Gejala Penyebab Solusi Realtime chat tidak update Reverb tidak running Cek php artisan reverb:start aktif (di local:start) Connection error WS Port 8080 tidak terjangkau / firewall Cek REVERB_PORT & firewall Channel authorization 403 Member bukan pemilik tiket / auth salah Cek routes/channels.php authorization logic Event tidak broadcast BROADCAST_DRIVER bukan reverbCek .env BROADCAST_DRIVER=reverb Crisp tidak muncul CRISP_ENABLED=false atau CRISP_WEBSITE_ID kosongEnable di .env, isi Website ID WA & Crisp tumpang tindih CSS .wa-float-dual tidak apply Audit layout, sesuaikan right offset
Langkah berikutnya
Detail Events & broadcasting? Lihat Conventions > Events .
Detail Crisp implementation? Baca CLAUDE.md repo dazo-whitelist (section Crisp).
Detail external services? Lihat External Services .