Keluhan pelanggan masuk dari WhatsApp, email, DM Instagram, dan kadang telepon. Tidak ada yang tahu siapa sedang menangani apa, keluhan yang sama dijawab dua kali, dan yang lain terlewat sampai pelanggan marah. Masalah ini bukan soal tim yang malas, tapi soal tidak adanya satu tempat untuk mencatat dan melacak setiap permintaan.
Sistem tiket menyelesaikan itu: setiap permintaan menjadi satu catatan dengan nomor, status, penanggung jawab, dan tenggat. Artikel ini membahas fitur yang benar-benar dibutuhkan, desain database, potongan kode Laravel 11 untuk bagian intinya, serta kapan lebih masuk akal memakai layanan helpdesk siap pakai.
Fitur Inti yang Wajib Ada
Jangan mulai dari daftar fitur helpdesk besar. Untuk tim 1-10 orang, enam hal ini sudah menutup sebagian besar kebutuhan:
- Nomor tiket unik yang mudah disebut, misalnya
TKT-2610-0042, agar pelanggan dan tim merujuk hal yang sama. - Status yang jelas: open, in progress, waiting customer, resolved, closed.
- Prioritas: low, normal, high, urgent, dengan definisi tertulis.
- Penugasan ke satu agen, bukan "tim", supaya ada yang bertanggung jawab.
- Percakapan berurutan (thread) termasuk catatan internal yang tidak terlihat pelanggan.
- SLA: tenggat respons pertama dan tenggat penyelesaian berdasarkan prioritas.
Fitur seperti basis pengetahuan, rating kepuasan, dan laporan bisa ditambahkan setelah fondasi ini berjalan.
Bangun Sendiri atau Pakai Helpdesk Jadi?
Sebelum menulis satu baris kode, putuskan dulu apakah Anda memang perlu membangun. Perbandingan berikut membantu:
| Kriteria | Helpdesk SaaS (Zendesk, Freshdesk, Zoho Desk) | Open source (osTicket, FreeScout) | Bangun di Laravel |
|---|---|---|---|
| Waktu siap pakai | Hitungan jam | 1-2 hari instalasi | Beberapa minggu |
| Biaya | Langganan per agen per bulan | Hosting saja | Waktu developer + hosting |
| Integrasi dengan data internal (order, member) | Lewat API/plugin, terbatas | Perlu modifikasi | Langsung ke tabel yang sama |
| Kustomisasi alur | Sebatas pengaturan | Bisa, tapi kode orang lain | Penuh |
| Cocok untuk | Tim support murni | Tim dengan budget minim | Tiket yang menempel ke produk/akun Anda |
Membangun sendiri masuk akal jika tiket harus terhubung langsung dengan data aplikasi, misalnya pelanggan membuka tiket dari halaman pesanan dan agen langsung melihat status pembayaran serta lisensi tanpa pindah sistem.
Sebaliknya, jika tim Anda hanya butuh kotak masuk bersama untuk email dan belum punya aplikasi sendiri, helpdesk SaaS atau FreeScout akan jauh lebih hemat waktu. Waktu developer yang dipakai membangun sistem tiket adalah waktu yang tidak dipakai untuk memperbaiki produk utama.
Desain Database
Tiga tabel sudah cukup untuk versi pertama: tickets, ticket_messages, dan relasi ke users yang sudah ada. Jika Anda belum terbiasa merancang relasi, baca dulu panduan merancang ERD sederhana.
// database/migrations/2026_10_01_000000_create_tickets_tables.php
Schema::create('tickets', function (Blueprint $table) {
$table->id();
$table->string('number', 20)->unique();
$table->foreignId('user_id')->constrained(); // pelanggan
$table->foreignId('assigned_to')->nullable()->constrained('users');
$table->string('subject');
$table->string('category', 50)->nullable();
$table->enum('priority', ['low', 'normal', 'high', 'urgent'])->default('normal');
$table->enum('status', ['open', 'in_progress', 'waiting_customer', 'resolved', 'closed'])->default('open');
$table->timestamp('first_response_due_at')->nullable();
$table->timestamp('resolution_due_at')->nullable();
$table->timestamp('first_responded_at')->nullable();
$table->timestamp('resolved_at')->nullable();
$table->timestamps();
$table->index(['status', 'priority']);
});
Schema::create('ticket_messages', function (Blueprint $table) {
$table->id();
$table->foreignId('ticket_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained();
$table->text('body');
$table->boolean('is_internal')->default(false); // catatan antar-agen
$table->timestamps();
});
Kolom first_responded_at dan resolved_at sengaja dipisah dari updated_at. Tanpa keduanya, Anda tidak bisa mengukur apakah SLA terpenuhi, karena updated_at berubah setiap ada perubahan kecil.
Nomor Tiket dan SLA Otomatis
Nomor tiket dan tenggat SLA sebaiknya dihitung di satu tempat agar konsisten, misalnya pada event creating di model.
// app/Models/Ticket.php
class Ticket extends Model
{
protected $guarded = [];
protected $casts = [
'first_response_due_at' => 'datetime',
'resolution_due_at' => 'datetime',
'first_responded_at' => 'datetime',
'resolved_at' => 'datetime',
];
// jam: [respons pertama, penyelesaian]
public const SLA_HOURS = [
'urgent' => [1, 8],
'high' => [4, 24],
'normal' => [8, 72],
'low' => [24, 120],
];
protected static function booted(): void
{
static::creating(function (Ticket $ticket) {
$ticket->number ??= 'TKT-' . now()->format('ym') . '-' . strtoupper(Str::random(5));
[$respond, $resolve] = self::SLA_HOURS[$ticket->priority ?? 'normal'];
$ticket->first_response_due_at = now()->addHours($respond);
$ticket->resolution_due_at = now()->addHours($resolve);
});
}
public function messages()
{
return $this->hasMany(TicketMessage::class);
}
}
Angka SLA di atas hanya contoh. Sesuaikan dengan kapasitas tim Anda yang sebenarnya. SLA yang tidak realistis lebih buruk daripada tidak ada SLA, karena tim akan terbiasa melanggarnya.
Catat respons pertama
Saat agen membalas (bukan catatan internal), isi first_responded_at jika masih kosong dan ubah status menjadi in_progress atau waiting_customer:
if (! $message->is_internal && $message->user_id !== $ticket->user_id) {
$ticket->first_responded_at ??= now();
$ticket->status = 'waiting_customer';
$ticket->save();
}
Notifikasi dan Pengingat SLA
Tiket yang tidak ada yang tahu sama saja dengan pesan WhatsApp yang tenggelam. Minimal kirim tiga jenis notifikasi:
- Ke pelanggan: tiket diterima (berisi nomor tiket), ada balasan baru, tiket diselesaikan.
- Ke agen: tiket baru ditugaskan kepadanya, pelanggan membalas.
- Ke supervisor: tiket mendekati atau melewati tenggat SLA.
Kirim email lewat queue agar halaman tidak lambat; penjelasannya ada di queue dan job di Laravel. Untuk pengingat SLA, jadwalkan pengecekan berkala di routes/console.php:
Schedule::call(function () {
Ticket::whereIn('status', ['open', 'in_progress'])
->whereNull('first_responded_at')
->where('first_response_due_at', '<=', now()->addMinutes(30))
->each(fn ($t) => Notification::route('mail', config('support.supervisor_email'))
->notify(new SlaWarning($t)));
})->everyFifteenMinutes();
Pada implementasi nyata, tambahkan kolom penanda (misalnya sla_warned_at) agar peringatan yang sama tidak terkirim berulang setiap 15 menit.
Kesalahan Umum
- Terlalu banyak status. Sepuluh status membuat agen bingung memilih. Lima sudah cukup.
- Prioritas dipilih pelanggan. Hampir semua pelanggan akan memilih "urgent". Biarkan pelanggan memilih kategori, lalu agen atau aturan otomatis yang menentukan prioritas.
- Tiket tanpa pemilik. Tiket yang ditugaskan ke "tim" cenderung tidak dikerjakan. Pastikan selalu ada satu nama.
- Tidak ada otorisasi. Pelanggan A tidak boleh bisa membuka tiket B hanya dengan mengganti ID di URL. Gunakan Policy dan cek
user_id; lihat juga best practice keamanan aplikasi web. - Menutup tiket otomatis terlalu cepat. Tiket berstatus "waiting customer" wajar ditutup otomatis setelah beberapa hari tanpa balasan, tapi beri tahu pelanggan sebelumnya.
Basis Pengetahuan untuk Mengurangi Tiket
Setelah sistem berjalan satu-dua bulan, kelompokkan tiket berdasarkan kategori. Pertanyaan yang muncul berulang, misalnya cara reset password atau cara instal aplikasi, layak dijadikan artikel bantuan. Tampilkan artikel yang relevan di form pembuatan tiket berdasarkan kata kunci subjek. Sebagian pelanggan akan menemukan jawabannya sendiri sebelum menekan tombol kirim.
Metrik yang Perlu Dipantau
Begitu tiket tercatat rapi, Anda punya data yang sebelumnya tidak terlihat. Empat angka ini cukup untuk rapat mingguan:
- Waktu respons pertama: selisih
created_atdanfirst_responded_at. Pakai median, bukan rata-rata, agar satu tiket yang terlupa tidak merusak gambaran. - Persentase SLA terpenuhi: jumlah tiket yang direspons sebelum
first_response_due_atdibagi total tiket. - Tiket terbuka per agen: membantu melihat siapa yang kelebihan beban sebelum kualitas turun.
- Kategori terbanyak: sinyal bagian produk mana yang paling membingungkan pelanggan.
Contoh: jika kategori "instalasi" mendominasi setiap minggu, perbaikan dokumentasi atau video panduan kemungkinan menurunkan volume tiket lebih banyak daripada menambah agen.
Checklist Peluncuran
- Definisi prioritas dan angka SLA sudah disepakati tim dan ditulis.
- Setiap tiket punya nomor unik dan satu penanggung jawab.
- Catatan internal tidak pernah terkirim ke pelanggan (uji secara manual).
- Policy mencegah pelanggan melihat tiket orang lain.
- Email dikirim lewat queue dan worker berjalan di server.
- Scheduler aktif untuk pengingat SLA.
- Kanal lama (WhatsApp, email) diberi pesan otomatis yang mengarahkan ke form tiket.
Sistem tiket yang sederhana tapi dipakai konsisten jauh lebih berguna daripada helpdesk lengkap yang diabaikan. Mulai dari fondasi di atas, ukur waktu respons selama sebulan, lalu tambahkan fitur berdasarkan data, bukan dugaan. Pola yang sama kami pakai untuk tiket member di GudangCode.