Dashboard sudah dibuat, grafiknya warna-warni, tetapi pemilik usaha tetap menelepon Anda untuk bertanya "bulan ini penjualan naik atau turun?". Itu tanda dashboard-nya menampilkan angka, bukan jawaban. Masalah kedua biasanya muncul belakangan: halaman dashboard makin lambat seiring data transaksi bertambah.
Artikel ini membahas dua sisi tersebut. Pertama, cara memilih metrik yang memandu keputusan. Kedua, cara membangunnya di Laravel 11 dengan query agregasi yang efisien, grafik Chart.js, cache, dan tabel ringkasan harian untuk data besar.
1. Mulai dari Pertanyaan, Bukan dari Grafik
Sebelum menulis query, duduklah dengan pemakai dashboard dan catat pertanyaan yang mereka ajukan setiap minggu. Untuk toko retail atau kafe, daftarnya biasanya seperti ini:
- Apakah pendapatan 30 hari terakhir lebih tinggi dari 30 hari sebelumnya?
- Produk apa yang paling laku, dan mana yang tidak bergerak?
- Jam dan hari apa yang paling ramai, untuk mengatur jadwal karyawan?
- Berapa rata-rata nilai satu transaksi (AOV)?
Aturan praktisnya: sebuah metrik layak dipasang jika ada keputusan yang berubah ketika angkanya berubah. Total pengunjung sepanjang masa tidak lolos tes ini karena angkanya selalu naik dan tidak memicu tindakan apa pun.
| Vanity metric | Pengganti yang bisa ditindaklanjuti | Keputusan yang dipandu |
|---|---|---|
| Total pengunjung sepanjang masa | Konversi pengunjung ke pembeli minggu ini | Perbaiki halaman produk atau promosi |
| Jumlah produk di katalog | Produk tanpa penjualan 60 hari | Diskon, bundling, atau stop restock |
| Total omzet tahun berjalan | Pendapatan 30 hari vs periode sebelumnya | Tambah atau kurangi anggaran iklan |
| Jumlah user terdaftar | Pelanggan yang belanja ulang dalam 90 hari | Program loyalitas atau follow-up |
Batasi tampilan utama pada empat sampai enam angka. Detail lain bisa diletakkan di halaman terpisah yang dibuka saat diperlukan.
2. Kartu Ringkasan dengan Pembanding
Angka tanpa pembanding sulit dimaknai. Rp12 juta itu bagus atau buruk? Tergantung bulan lalu. Biarkan database yang menghitung; jangan mengambil ribuan baris lalu menjumlahkannya di PHP.
use App\Models\Order;
$now = now();
$start = $now->copy()->subDays(30);
$prev = $now->copy()->subDays(60);
$current = Order::where('created_at', '>=', $start)
->selectRaw('COUNT(*) as orders, COALESCE(SUM(total), 0) as revenue')
->first();
$previousRevenue = Order::whereBetween('created_at', [$prev, $start])->sum('total');
$aov = $current->orders > 0 ? $current->revenue / $current->orders : 0;
$growth = $previousRevenue > 0
? round(($current->revenue - $previousRevenue) / $previousRevenue * 100, 1)
: null; // null = belum ada data pembanding
Perhatikan dua hal. Jumlah order dan pendapatan diambil dalam satu query, bukan dua. Lalu pertumbuhan bernilai null saat periode sebelumnya kosong, sehingga tampilan bisa menulis "belum ada pembanding" alih-alih "0%" yang menyesatkan.
3. Deret Waktu Tanpa Tanggal Bolong
$daily = Order::where('created_at', '>=', $start)
->selectRaw('DATE(created_at) as day, SUM(total) as revenue')
->groupBy('day')
->orderBy('day')
->pluck('revenue', 'day');
$labels = [];
$values = [];
for ($i = 29; $i >= 0; $i--) {
$date = now()->subDays($i)->toDateString();
$labels[] = $date;
$values[] = (int) ($daily[$date] ?? 0);
}
Hari tanpa transaksi tidak muncul di hasil GROUP BY. Tanpa loop pengisi nol, Chart.js akan menarik garis langsung dari Senin ke Kamis dan grafik terlihat seolah tidak ada hari libur. Pastikan juga zona waktu aplikasi di config/app.php sesuai lokasi usaha, misalnya Asia/Jakarta, agar transaksi pukul 23.30 tidak tercatat di hari berikutnya.
4. Produk Terlaris dan Jam Ramai
$topProducts = OrderItem::query()
->join('orders', 'orders.id', '=', 'order_items.order_id')
->where('orders.created_at', '>=', $start)
->selectRaw('order_items.product_name, SUM(order_items.qty) as sold')
->groupBy('order_items.product_name')
->orderByDesc('sold')
->limit(5)
->get();
$busyHours = Order::where('created_at', '>=', $start)
->selectRaw('HOUR(created_at) as hour, COUNT(*) as orders')
->groupBy('hour')
->orderBy('hour')
->pluck('orders', 'hour');
Fungsi HOUR() dan DATE() di atas berlaku untuk MySQL dan MariaDB. Di PostgreSQL gunakan EXTRACT(HOUR FROM created_at) dan created_at::date.
5. Mengirim Data ke Chart.js
<canvas id="revenueChart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
new Chart(document.getElementById('revenueChart'), {
type: 'line',
data: {
labels: @json($labels),
datasets: [{ label: 'Pendapatan harian', data: @json($values), tension: 0.3 }]
}
});
</script>
Direktif @json meng-encode array dengan aman sehingga tanda kutip di nama produk tidak merusak JavaScript.
6. Menjaga Dashboard Tetap Cepat
Pilih strategi sesuai ukuran data. Angka di bawah hanya perkiraan kasar untuk server VPS kecil; ukur sendiri dengan data Anda.
| Ukuran tabel order (perkiraan) | Strategi |
|---|---|
| Puluhan ribu baris | Query langsung + index pada created_at |
| Ratusan ribu baris | Tambahkan Cache::remember 5–15 menit |
| Jutaan baris | Tabel ringkasan harian yang diisi cron |
Index adalah langkah pertama yang paling murah. Tambahkan lewat migration, lalu ikuti tips lain di artikel optimasi performa query Eloquent.
Schema::table('orders', function (Blueprint $table) {
$table->index('created_at');
});
Untuk data jutaan baris, simpan agregat per hari di tabel daily_sales (kolom date unik, orders, revenue), lalu isi dengan command terjadwal:
// routes/console.php
use App\Models\Order;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
$day = now()->subDay()->toDateString();
$row = Order::whereDate('created_at', $day)
->selectRaw('COUNT(*) as orders, COALESCE(SUM(total), 0) as revenue')
->first();
DB::table('daily_sales')->updateOrInsert(
['date' => $day],
['orders' => $row->orders, 'revenue' => $row->revenue, 'updated_at' => now()]
);
})->dailyAt('00:15');
Dashboard kemudian cukup membaca 30 baris dari daily_sales ditambah data hari ini secara langsung. Jika pekerjaan agregasinya berat, pindahkan ke job antrean seperti dibahas di Queue dan Job di Laravel.
7. Tata Letak yang Membantu Membaca
Mata pemakai bergerak dari kiri atas ke kanan bawah. Manfaatkan urutan itu:
- Baris pertama: kartu KPI utama, masing-masing dengan panah naik atau turun dan persentase perubahan.
- Baris kedua: grafik tren pendapatan 30 hari, cukup satu garis.
- Baris ketiga: tabel produk terlaris dan grafik batang jam ramai berdampingan.
- Paling bawah: tautan ke laporan detail, filter rentang tanggal, dan tombol ekspor.
Gunakan warna dengan hemat. Hijau dan merah cukup untuk naik dan turun; jangan memberi warna berbeda pada setiap kartu hanya demi variasi. Di layar HP, susun kartu menjadi satu kolom karena pemilik usaha sering mengecek dashboard dari ponsel di sela kesibukan.
8. Batasi Siapa yang Bisa Melihat
Angka omzet dan margin bukan informasi untuk semua karyawan. Definisikan Gate di AppServiceProvider, lalu pasang di route:
// app/Providers/AppServiceProvider.php, di method boot()
Gate::define('view-dashboard', fn (User $user) => in_array($user->role, ['owner', 'manager']));
// routes/web.php
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware(['auth', 'can:view-dashboard']);
Kolom role di contoh ini hanyalah asumsi; sesuaikan dengan struktur tabel user Anda.
Kesalahan Umum dan Perbaikannya
- Terlalu banyak grafik di satu layar. Pemakai tidak tahu harus melihat ke mana. Pangkas menjadi empat sampai enam KPI, sisanya pindah ke halaman detail.
- Jenis grafik salah. Tren waktu pakai garis, perbandingan antar-produk pakai batang horizontal. Pie chart dengan sepuluh potongan nyaris tidak terbaca.
- Menghitung transaksi batal. Filter status, misalnya hanya
paid, agar omzet tidak menggelembung. - Cache tidak pernah diperbarui. Panggil
Cache::forget()di event pembuatan order bila angka harus segar. - Angka tidak pernah dicocokkan. Satu selisih kecil dengan laporan kasir membuat pemilik berhenti percaya pada seluruh dashboard. Cocokkan sampel sebelum serah terima.
Checklist Sebelum Dashboard Diserahkan
- Setiap KPI punya pembanding periode sebelumnya atau target.
- Rentang tanggal dan zona waktu tertulis jelas di layar.
- Tanggal tanpa transaksi muncul sebagai nol.
- Halaman termuat di bawah dua detik dengan data nyata.
- Angka dicocokkan manual dengan laporan kasir untuk satu hari sampel.
- Pemilik usaha bisa menjawab pertanyaan mingguannya tanpa bertanya ke developer.
Dashboard yang berguna bukan yang paling ramai grafiknya, melainkan yang membuat keputusan lebih cepat diambil. Konsep yang sama juga berlaku saat Anda mendigitalisasi bisnis kecil dengan sistem informasi: data baru bernilai ketika dipakai untuk bertindak.