Cara Membuat Sistem Tiket Pengaduan dengan SLA di PHP & MySQL (Studi Kasus e-Lapor)
Aplikasi pengaduan (helpdesk, layanan publik, e-Lapor) punya tiga kebutuhan yang sama: setiap laporan harus bisa dilacak, penanganannya harus tercatat, dan semua pihak harus tahu kalau ada laporan yang terlambat ditangani.
Di tutorial ini kita bangun intinya dengan PHP & MySQL: nomor tiket, timeline tanggapan dengan catatan internal, perhitungan SLA, sampai pembatasan akses per unit. Contohnya diambil dari aplikasi Sistem Pengaduan Masyarakat (e-Lapor) yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Struktur tabel
CREATE TABLE kategori (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(80) NOT NULL,
unit_id INT NULL, -- unit penanggung jawab default
sla_hari TINYINT NOT NULL DEFAULT 7
) ENGINE=InnoDB;
CREATE TABLE aduan (
id INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(20) NOT NULL UNIQUE, -- ADU-2610-0287
kategori_id INT, unit_id INT,
judul VARCHAR(150) NOT NULL, isi TEXT NOT NULL,
status ENUM('baru','diverifikasi','diproses','selesai','ditolak') NOT NULL DEFAULT 'baru',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
selesai_at DATETIME NULL
) ENGINE=InnoDB;
CREATE TABLE tanggapan (
id INT AUTO_INCREMENT PRIMARY KEY,
aduan_id INT NOT NULL,
user_id INT NULL,
status VARCHAR(20) NOT NULL,
isi TEXT NOT NULL,
internal TINYINT() ,
created_at DATETIME
) ENGINEInnoDB;
SLA disimpan per kategori, bukan per aduan. Sampah menumpuk butuh penanganan cepat (3 hari), sedangkan jalan rusak wajar lebih lama (14 hari).
2. Nomor tiket yang tidak bisa ditebak urutannya
Cara paling aman: insert dulu memakai kode sementara, lalu bentuk kode final dari id yang dijamin unik oleh database.
<?php
$pdo->prepare('INSERT INTO aduan (kode, kategori_id, unit_id, judul, isi) VALUES (?,?,?,?,?)')
->execute(['TMP-' . bin2hex(random_bytes(4)), $kategoriId, $unitId, $judul, $isi]);
$id = (int) $pdo->lastInsertId();
$kode = 'ADU-' . date('ym') . '-' . str_pad((string) $id, 4, '0', STR_PAD_LEFT); // ADU-2610-0287
$pdo->prepare('UPDATE aduan SET kode = ? WHERE id = ?')->execute([$kode, $id]);
Hindari SELECT MAX(id) + 1 sebelum insert. Dua pelapor yang mengirim bersamaan bisa mendapat nomor yang sama.
3. Routing otomatis ke unit penanggung jawab
Saat laporan masuk, ambil unit_id dari kategorinya. Petugas unit langsung bisa melihat laporan tanpa perlu menunggu admin memilah satu per satu:
$st = $pdo->prepare('SELECT * FROM kategori WHERE id = ?');
$st->execute([$_POST['kategori_id']]);
$kat = $st->fetch();
$unitId = $kat['unit_id']; // misal: kategori "Jalan Rusak" -> Dinas PU
4. Timeline tanggapan dan catatan internal
Setiap perubahan status dan tanggapan disimpan sebagai baris baru di tabel tanggapan, jangan menimpa kolom di tabel aduan. Dengan begitu riwayatnya utuh. Untuk halaman publik, cukup saring internal = 0:
// Halaman lacak tiket (publik): catatan internal tidak ikut
$st = $pdo->prepare('SELECT * FROM tanggapan WHERE aduan_id = ? AND internal = 0 ORDER BY created_at');
// Panel petugas: tampilkan semuanya
$st = $pdo->prepare('SELECT * FROM tanggapan WHERE aduan_id = ? ORDER BY created_at');
Saat status berubah menjadi selesai atau ditolak, isi selesai_at. Kolom ini nanti dipakai untuk menghitung lama penanganan.
5. Menghitung umur aduan dan lewat SLA
<?php
function umur_hari(array $aduan): int
{
// Kalau sudah selesai hitung sampai selesai_at, kalau belum hitung sampai sekarang
$akhir = $aduan['selesai_at'] ?: date('Y-m-d H:i:s');
return (int) floor((strtotime($akhir) - strtotime($aduan['created_at'])) / 86400);
}
function lewat_sla(array $aduan): bool
{
$masihTerbuka = !in_array($aduan['status'], ['selesai', 'ditolak'], true);
return $masihTerbuka && umur_hari($aduan) > (int) $aduan['sla_hari'];
}
Untuk laporan kinerja, hitung langsung di SQL berapa persen aduan yang selesai tepat SLA per unit:
SELECT u.nama,
SUM(a.status = 'selesai') AS selesai,
SUM(a.status = 'selesai' AND TIMESTAMPDIFF(HOUR, a.created_at, a.selesai_at) <= k.sla_hari * 24) AS tepat_sla,
ROUND(AVG(CASE WHEN a.status = 'selesai'
THEN TIMESTAMPDIFF(HOUR, a.created_at, a.selesai_at) / 24 END), 1) AS rata_hari
FROM unit u
LEFT JOIN aduan a ON a.unit_id = u.id
LEFT JOIN kategori k ON k.id = a.kategori_id
GROUP BY u.id;
6. Batasi akses petugas di sisi server
Petugas Dinas Lingkungan Hidup tidak boleh membuka atau mengubah aduan milik Dinas PU. Jangan hanya menyembunyikan tombolnya di HTML. Cek ulang di setiap request:
<?php
$aduan = ambil_aduan($_GET['id']);
$user = $_SESSION['user'];
if ($user['role'] !== 'admin' && $aduan['unit_id'] != $user['unit_id']) {
http_response_code(403);
exit('Aduan ini bukan wewenang unit Anda.');
}
7. Bonus: anti spam untuk form publik tanpa CAPTCHA
Form publik pasti diserbu bot. Dua cara ringan yang cukup efektif:
<!-- Honeypot: tidak terlihat oleh manusia, bot biasanya mengisinya -->
<div style="position:absolute;left:-9999px">
<input type="text" name="website" tabindex="-1" autocomplete="off">
</div>
<?php
if (!empty($_POST['website'])) exit; // hampir pasti bot
// Maksimal 5 laporan per sesi per jam
$_SESSION['log'] = array_filter($_SESSION['log'] ?? [], fn ($t) => $t > time() - 3600);
if (count($_SESSION['log']) >= 5) exit('Terlalu banyak laporan, coba lagi nanti.');
$_SESSION['log'][] = time();
Ringkasan
- Simpan SLA per kategori dan unit penanggung jawab di tabel kategori.
- Bentuk nomor tiket dari id setelah insert supaya dijamin unik.
- Catat setiap tindak lanjut sebagai baris baru di tabel tanggapan, dengan flag
internal. - Hitung umur dan lewat SLA dari
created_atdanselesai_at. - Batasi akses per unit di server, dan lindungi form publik dengan honeypot serta rate limit.
Download source code lengkapnya
Semua logika di atas sudah dipakai di Sistem Pengaduan Masyarakat (e-Lapor) PHP Native & MySQL: portal warga dengan foto dan laporan anonim, lacak tiket dengan rating kepuasan, routing ke dinas, SLA, catatan internal, laporan kinerja per unit, dan halaman transparansi publik. Gratis dan langsung jalan di XAMPP.
Download Source Code Sistem Pengaduan Masyarakat di Papicode
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.