D
Engineering

Realtime Ticketing

Sistem tiket Konco dengan live chat real-time via Laravel Reverb WebSocket. Events TicketMessageCreated/TicketStatusUpdated. Channel private per tiket. Crisp Chat dual-channel (WhatsApp + Crisp) dengan z-index hierarchy. Ticket lifecycle via model booted hooks.

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

AspekKeterangan
Packagelaravel/reverb ^1.0
Serverphp artisan reverb:start (di-start local:start)
PortREVERB_PORT=8080 (default)
ProtocolWebSocket, kompatibel Pusher JS
DriverBROADCAST_DRIVER=reverb
FrontendLaravel Echo + pusher-js — config VITE_REVERB_*
Configconfig/reverb.php

Event yang di-broadcast

EventTriggerChannelPayload
TicketMessageCreatedPesan tiket dikirimprivate-ticket.{ticket_id}TicketMessage instance
TicketStatusUpdatedStatus tiket berubahprivate-ticket.{ticket_id}Ticket old/new status
AdminRequestCreatedAdmin request baruprivate-admin-request.{admin_id}AdminRequest instance
AdminSubscriptionReminderNotificationOrder paid belum diprosesPrivate admin channelOrder instance
AdminWithdrawReminderNotificationWithdraw belum selesaiPrivate admin channelOrder instance

Events berlokasi di app/Events/.

Channel authorization

Broadcast channel authorization di routes/channels.php:

php
// private-ticket.{ticket_id}
Broadcast::channel('ticket.{id}', function ($user, $id) {
    $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');
});

// private-admin-request.{admin_id}
Broadcast::channel('admin-request.{id}', function ($user, $id) {
    return $user->id === $id || $user->guard_name === 'admin';
});

Ticket model lifecycle

Ticket model (app/Models/Ticket.php) menggunakan booted() hooks untuk side effects:

creating — auto-set admin_id null

php
static::creating(function(Ticket $e) {
    // Do not auto-assign admin on create.
    // admin_id should be set explicitly when a ticket is assigned.
    if (!$e->admin_id) {
        $e->admin_id = null;
    }
});

created — log activity

php
static::created(function(Ticket $e) {
    $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",
    ]);
});

updated — notification side effects

TriggerNotificationEmail
admin_id berubah & tidak nullTicketHandledNotification to member + AdminTicketAssignedNotification to adminemail.ticket.ongoing
status berubah ke closedTicketClosedNotification to memberemail.ticket.closed
status berubah ke open & reopened_at setTicketReopenedNotification to memberemail.ticket.reopened

deleting — cascade

php
static::deleting(function(Ticket $e) {
    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();
});

Ticket status lifecycle

text
[pending] ──admin assign──► [open/on progress] ──admin close──► [closed]
                                  ▲                                 │
                                  │                                 │
                                  └──reopen─────────────────────────┘
StatusArtiBadge color
pendingTiket baru, belum ditugaskan adminblue
openSudah ditugaskan, dalam progressinfo/warning
on progressSedang dikerjakan adminwarning
closedSelesai/ditutupred

Ticket auto-assignment

php
static function getadmin() {
    $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();
}

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

ConfigEnvKeterangan
Website IDCRISP_WEBSITE_IDID dari Crisp dashboard
EnabledCRISP_ENABLEDToggle on/off
Localehardcoded "id"Bahasa Indonesia
Theme colorhardcoded #2563ebMatch project theme
Nicknamehardcoded “Support Team”Default team name

Z-index hierarchy (dual-channel dengan WhatsApp)

ElementZ-IndexPosisi
Crisp widget1000Pojok kanan bawah
WhatsApp button999Digeser kiri Crisp
Other CTA998—
css
/* WhatsApp button digeser ke kiri Crisp */
.wa-float-dual {
    right: 95px !important;
    bottom: 20px !important;
    z-index: 999;
}

.crisp-client {
    z-index: 1000 !important;
}

User identity sync

Saat member login, Crisp otomatis dapat identitas:

javascript
window.CRISP_READY_TRIGGER = function() {
    $crisp.push(["set", "user:email", ["{{ auth()->user()->email }}"]]);
    $crisp.push(["set", "user:nickname", ["{{ auth()->user()->name }}"]]);
};

Notification channels

NotificationChannelsPenerima
TicketCreatedNotificationdatabase + broadcastAdmin
TicketHandledNotificationdatabase + broadcast + mailMember
TicketClosedNotificationdatabase + broadcast + mailMember
TicketReopenedNotificationdatabase + broadcast + mailMember
AdminTicketNewNotificationdatabase + broadcastAdmin
AdminTicketAssignedNotificationdatabase + broadcastAdmin

Frontend implementation

Echo initialization

javascript
// resources/js/app.js (disederhanakan)
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Echo = new Echo({
    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',
});

Subscribe ticket channel

javascript
Echo.private(`ticket.${ticketId}`)
    .listen('TicketMessageCreated', (e) => {
        // Append message to chat UI
    })
    .listen('TicketStatusUpdated', (e) => {
        // Update ticket status badge
    });

Troubleshooting realtime

GejalaPenyebabSolusi
Realtime chat tidak updateReverb tidak runningCek php artisan reverb:start aktif (di local:start)
Connection error WSPort 8080 tidak terjangkau / firewallCek REVERB_PORT & firewall
Channel authorization 403Member bukan pemilik tiket / auth salahCek routes/channels.php authorization logic
Event tidak broadcastBROADCAST_DRIVER bukan reverbCek .env BROADCAST_DRIVER=reverb
Crisp tidak munculCRISP_ENABLED=false atau CRISP_WEBSITE_ID kosongEnable di .env, isi Website ID
WA & Crisp tumpang tindihCSS .wa-float-dual tidak applyAudit 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.