Materi Lengkap: Membuat Dashboard Admin dengan Statistik dan Grafik di Laravel
1. Persiapan dan Struktur Routing
A. Update Routing File (routes/web.php)
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\DashboardController;
// --- ROUTE PUBLIK YANG SUDAH ADA SEBELUMNYA (TANPA MIDDLEWARE) ---
Route::get('/', [HomeController::class, 'home']);
Route::get('/about', [HomeController::class, 'about']);
Route::get('/contact', [HomeController::class, 'contact']);
// ROUTE LOGIN/LOGOUT (harus di luar middleware auth)
Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
// ==================== ROUTE PRIVAT (HARUS LOGIN) ====================
// Semua route di dalam grup ini dilindungi middleware 'auth'
Route::middleware(['auth'])->group(function () {
// -------------------- DASHBOARD ROUTES --------------------
Route::prefix('dashboard')->name('dashboard.')->group(function () {
// Dashboard utama
Route::get('/', [DashboardController::class, 'index'])->name('index');
// Data untuk AJAX/refresh
Route::get('/data', [DashboardController::class, 'getDashboardData'])->name('data');
});
// -------------------- PRODUCT ROUTES --------------------
// Contoh: Route untuk manajemen produk
Route::get('products/chart', [ProductController::class, 'chart'])->name('products.chart'); //Untuk Chart /Grafik
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
Route::get('/products/create', [ProductController::class, 'create'])->name('products.create');
Route::post('/products', [ProductController::class, 'store'])->name('products.store');
Route::get('/products/{product}/edit', [ProductController::class, 'edit'])->name('products.edit');
Route::put('/products/{product}', [ProductController::class, 'update'])->name('products.update');
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.destroy')
});
B. Penjelasan Struktur Routing
ROUTING STRUCTURE:
├── 📍 PUBLIC ROUTES (Tanpa login)
│ ├── GET / → Welcome page
│ ├── GET /about → About page
│ ├── GET /about → Contact page
│ ├── GET /login → Form login
│ └── POST /login → Proses login
│
├── 🔒 PRIVATE ROUTES (Harus login) ← Dilindungi middleware 'auth'
│ │
│ ├── 📊 DASHBOARD GROUP
│ │ ├── GET /dashboard → Dashboard utama
│ │ ├── GET /dashboard/data → Data AJAX
│ │
│ └── 📦 PRODUCTS GROUP
│ ├── GET /products → List produk
│ ├── GET /products/create → Form tambah
│ ├── POST /products → Simpan produk
│ ├── GET /products/{id} → Detail produk
│ ├── GET /products/{id}/edit → Form edit
│ ├── PUT /products/{id} → Update produk
│ ├── DELETE /products/{id} → Hapus produk
│ └── GET /products/chart → Grafik produk
2. Dashboard Controller Lengkap
File: app/Http/Controllers/DashboardController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DashboardController extends Controller
{
// ==================== METHOD UTAMA ====================
/**
* Menampilkan halaman dashboard utama
* Route: GET /dashboard
*/
public function index()
{
// 1. Ambil semua data yang diperlukan
$data = $this->prepareDashboardData();
// 2. Kirim data ke view
return view('dashboard', $data);
}
/**
* API untuk mengambil data dashboard (AJAX refresh)
* Route: GET /dashboard/data
*/
public function getDashboardData()
{
$data = $this->prepareDashboardData();
return response()->json([
'success' => true,
'data' => $data,
'timestamp' => now()->format('Y-m-d H:i:s')
]);
}
// ==================== HELPER METHODS ====================
/**
* Menyiapkan semua data untuk dashboard
*/
private function prepareDashboardData()
{
return [
'stats' => $this->getDashboardStats(),
'chartData' => $this->getStockChartData(),
'lowStockProducts' => $this->getLowStockProducts(),
'highStockProducts' => $this->getHighStockProducts(),
'outOfStockProducts' => $this->getOutOfStockProducts(),
'recentProducts' => $this->getRecentProducts(),
];
}
/**
* Mengambil data statistik utama
*/
private function getDashboardStats()
{
return [
'total_products' => Product::count(),
'total_users' => User::count(),
'total_stock' => Product::sum('stock') ?? 0,
'total_value' => Product::sum(DB::raw('price * stock')) ?? 0,
'average_price' => Product::avg('price') ?? 0,
'out_of_stock' => Product::where('stock', 0)->count(),
'low_stock' => Product::where('stock', '<', 10)
->where('stock', '>', 0)
->count(),
];
}
/**
* Menyiapkan data untuk grafik Chart.js
*/
private function getStockChartData()
{
// Ambil 10 produk dengan stok terbanyak
$products = Product::orderBy('stock', 'desc')
->limit(10)
->get(['name', 'stock', 'price']);
// Format data
$labels = $products->pluck('name')->map(function ($name) {
return strlen($name) > 15 ? substr($name, 0, 15) . '...' : $name;
})->toArray();
$stocks = $products->pluck('stock')->toArray();
$prices = $products->pluck('price')->toArray();
// Tentukan warna berdasarkan level stok
$backgroundColors = [];
$borderColors = [];
foreach ($stocks as $stock) {
if ($stock > 50) {
// Hijau untuk stok tinggi
$backgroundColors[] = 'rgba(75, 192, 192, 0.2)';
$borderColors[] = 'rgba(75, 192, 192, 1)';
} elseif ($stock > 20) {
// Kuning untuk stok menengah
$backgroundColors[] = 'rgba(255, 206, 86, 0.2)';
$borderColors[] = 'rgba(255, 206, 86, 1)';
} elseif ($stock > 0) {
// Merah untuk stok rendah
$backgroundColors[] = 'rgba(255, 99, 132, 0.2)';
$borderColors[] = 'rgba(255, 99, 132, 1)';
} else {
// Abu-abu untuk stok habis
$backgroundColors[] = 'rgba(150, 150, 150, 0.2)';
$borderColors[] = 'rgba(150, 150, 150, 1)';
}
}
return [
'labels' => $labels,
'stocks' => $stocks,
'prices' => $prices,
'backgroundColors' => $backgroundColors,
'borderColors' => $borderColors
];
}
/**
* Mengambil produk dengan stok rendah (< 10)
*/
private function getLowStockProducts()
{
return Product::where('stock', '<', 10)
->where('stock', '>', 0)
->orderBy('stock', 'asc')
->limit(5)
->get();
}
/**
* Mengambil produk dengan stok terbanyak
*/
private function getHighStockProducts()
{
return Product::orderBy('stock', 'desc')
->limit(5)
->get();
}
/**
* Mengambil produk yang habis stok
*/
private function getOutOfStockProducts()
{
return Product::where('stock', 0)
->latest()
->limit(5)
->get();
}
/**
* Mengambil produk terbaru
*/
private function getRecentProducts()
{
return Product::latest()
->limit(5)
->get();
}
}
3. View Dashboard yang Diperbaiki
File: resources/views/dashboard.blade.php
@extends('layouts.main')
@section('title', 'Dashboard Admin')
@section('content')
<div class="container-fluid px-4">
<!-- HEADER -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800">📊 Dashboard Admin</h1>
<p class="mb-0">Selamat datang, {{ Auth::user()->name }}!</p>
</div>
<div class="text-end">
<div class="small text-muted">
<i class="fas fa-calendar me-1"></i>
{{ now()->translatedFormat('l, d F Y') }}
</div>
<div class="small text-muted">
<i class="fas fa-clock me-1"></i>
<span id="currentTime">{{ now()->format('H:i:s') }}</span>
</div>
</div>
</div>
<!-- STATISTIK CARDS -->
<div class="row">
<!-- Total Produk -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Total Produk
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ $stats['total_products'] }}
</div>
<div class="mt-2">
<a href="{{ route('products.index') }}" class="small text-primary text-decoration-none">
<i class="fas fa-eye me-1"></i> Lihat semua
</a>
</div>
</div>
<div class="col-auto">
<i class="fas fa-box fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Total Stok -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Total Stok
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ number_format($stats['total_stock']) }}
</div>
<div class="small text-muted mt-1">Unit tersedia</div>
</div>
<div class="col-auto">
<i class="fas fa-cubes fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Nilai Stok -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Nilai Stok
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
Rp{{ number_format($stats['total_value'], 0, ',', '.') }}
</div>
<div class="small text-muted mt-1">
Rata: Rp{{ number_format($stats['average_price'], 0, ',', '.') }}
</div>
</div>
<div class="col-auto">
<i class="fas fa-money-bill-wave fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Produk Habis -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-danger shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">
Produk Habis
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ $stats['out_of_stock'] }}
</div>
<div class="mt-2">
@if($stats['out_of_stock'] > 0)
<a href="#outOfStockSection" class="small text-danger text-decoration-none">
<i class="fas fa-exclamation-circle me-1"></i> Lihat daftar
</a>
@else
<span class="small text-success">
<i class="fas fa-check-circle me-1"></i> Semua stok tersedia
</span>
@endif
</div>
</div>
<div class="col-auto">
<i class="fas fa-times-circle fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- GRAFIK DAN PERINGATAN -->
<div class="row">
<!-- Grafik -->
<div class="col-xl-8 mb-4">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-bar me-1"></i>
Grafik 10 Produk dengan Stok Terbanyak
</h6>
</div>
<div class="card-body">
<div class="chart-container" style="position: relative; height:300px; width:100%">
<canvas id="stockChart"></canvas>
</div>
</div>
<div class="card-footer text-muted small">
<div class="d-flex justify-content-between">
<span>Update: <span id="chartUpdateTime">{{ now()->format('H:i:s') }}</span></span>
<span>Total {{ $stats['total_products'] }} produk</span>
</div>
</div>
</div>
</div>
<!-- Peringatan Stok Rendah -->
<div class="col-xl-4 mb-4">
<div class="card border-warning shadow">
<div class="card-header bg-warning text-dark py-3">
<h6 class="m-0 font-weight-bold">
<i class="fas fa-exclamation-triangle me-1"></i>
Stok Rendah <span class="badge bg-dark">{{ $lowStockProducts->count() }}</span>
</h6>
</div>
<div class="card-body">
@if($lowStockProducts->count() > 0)
<div class="alert alert-warning small">
<i class="fas fa-info-circle me-1"></i>
Ada {{ $lowStockProducts->count() }} produk dengan stok < 10
</div>
<div class="list-group">
@foreach($lowStockProducts as $product)
<a href="{{ route('products.edit', $product) }}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold">{{ Str::limit($product->name, 20) }}</div>
<small class="text-muted">ID: {{ $product->id }}</small>
</div>
<span class="badge bg-danger rounded-pill">{{ $product->stock }}</span>
</a>
@endforeach
</div>
@else
<div class="text-center py-4">
<div class="text-success mb-2">
<i class="fas fa-check-circle fa-3x"></i>
</div>
<h6 class="text-success">Stok Aman</h6>
<p class="small text-muted">Tidak ada stok rendah</p>
</div>
@endif
</div>
</div>
</div>
</div>
<!-- PRODUK TERBARU DAN HABIS -->
<div class="row">
<!-- Produk Terbaru -->
<div class="col-lg-6 mb-4">
<div class="card shadow">
<div class="card-header py-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-success">
<i class="fas fa-history me-1"></i>
Produk Terbaru
</h6>
<a href="{{ route('products.create') }}" class="btn btn-sm btn-success">
<i class="fas fa-plus me-1"></i> Baru
</a>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Produk</th>
<th class="text-center">Stok</th>
<th>Harga</th>
<th>Tanggal</th>
</tr>
</thead>
<tbody>
@foreach($recentProducts as $product)
<tr>
<td>
<div class="fw-bold">{{ Str::limit($product->name, 20) }}</div>
<small class="text-muted">ID: {{ $product->id }}</small>
</td>
<td class="text-center">
<span class="badge bg-{{ $product->stock > 10 ? 'success' : 'warning' }}">
{{ $product->stock }}
</span>
</td>
<td>Rp{{ number_format($product->price, 0, ',', '.') }}</td>
<td>
<div class="small">{{ $product->created_at->format('d/m/y') }}</div>
<div class="text-muted smaller">{{ $product->created_at->diffForHumans() }}</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Produk Habis Stok -->
<div class="col-lg-6 mb-4">
<div class="card border-danger shadow" id="outOfStockSection">
<div class="card-header bg-danger text-white py-3">
<h6 class="m-0 font-weight-bold">
<i class="fas fa-times-circle me-1"></i>
Produk Habis Stok <span class="badge bg-white text-danger">{{ $outOfStockProducts->count() }}</span>
</h6>
</div>
<div class="card-body">
@if($outOfStockProducts->count() > 0)
<div class="alert alert-danger small">
<i class="fas fa-exclamation-triangle me-1"></i>
Ada {{ $outOfStockProducts->count() }} produk habis stok
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Produk</th>
<th>Harga</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@foreach($outOfStockProducts as $product)
<tr>
<td>
<div class="fw-bold">{{ Str::limit($product->name, 20) }}</div>
<small class="text-muted">ID: {{ $product->id }}</small>
</td>
<td>Rp{{ number_format($product->price, 0, ',', '.') }}</td>
<td>
<a href="{{ route('products.edit', $product) }}"
class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="text-center py-4">
<div class="text-success mb-2">
<i class="fas fa-check-circle fa-3x"></i>
</div>
<h6 class="text-success">Stok Lengkap</h6>
<p class="small text-muted">Tidak ada produk yang habis</p>
</div>
@endif
</div>
</div>
</div>
</div>
<!-- TOP 5 STOK TERBANYAK -->
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-trophy me-1"></i>
Top 5 Produk dengan Stok Terbanyak
</h6>
</div>
<div class="card-body">
<div class="row">
@foreach($highStockProducts as $index => $product)
<div class="col-lg mb-3">
<div class="card h-100">
<div class="card-body text-center">
<!-- Ranking Badge -->
<div class="mb-3">
@if($index == 0)
<span class="badge bg-warning text-dark px-3 py-2">
<i class="fas fa-crown me-1"></i> Juara 1
</span>
@elseif($index == 1)
<span class="badge bg-secondary px-3 py-2">
<i class="fas fa-medal me-1"></i> Juara 2
</span>
@elseif($index == 2)
<span class="badge bg-danger px-3 py-2">
<i class="fas fa-medal me-1"></i> Juara 3
</span>
@else
<span class="badge bg-primary px-3 py-2">
#{{ $index + 1 }}
</span>
@endif
</div>
<!-- Product Info -->
<h6 class="card-title">{{ Str::limit($product->name, 18) }}</h6>
<div class="display-5 text-success fw-bold my-2">{{ $product->stock }}</div>
<div class="small text-muted mb-3">unit tersedia</div>
<!-- Actions -->
<a href="{{ route('products.edit', $product) }}"
class="btn btn-sm btn-outline-success">
<i class="fas fa-edit me-1"></i> Kelola
</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 1. UPDATE WAKTU
function updateTime() {
const now = new Date();
const timeString = now.toLocaleTimeString('id-ID');
document.getElementById('currentTime').textContent = timeString;
document.getElementById('chartUpdateTime').textContent = timeString;
}
setInterval(updateTime, 1000);
// 2. GRAFIK
const ctx = document.getElementById('stockChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: @json($chartData['labels']),
datasets: [{
label: 'Stok',
data: @json($chartData['stocks']),
backgroundColor: @json($chartData['backgroundColors']),
borderColor: @json($chartData['borderColors']),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value.toLocaleString('id-ID');
}
}
}
}
}
});
// 3. AJAX REFRESH (opsional)
document.getElementById('refreshBtn')?.addEventListener('click', function() {
fetch('{{ route("dashboard.data") }}')
.then(response => response.json())
.then(data => {
console.log('Data refreshed:', data);
alert('Dashboard diperbarui!');
});
});
</script>
@endpush
4. Ringkasan dan Cara Penggunaan
A. Langkah-Langkah Implementasi:
1.Buat Controller Dashboard:
php artisan make:controller DashboardController
2.Copy kode controller di atas ke file tersebut
3.Update routes/web.php dengan routing yang sudah disediakan
4.Buat file view dashboard.blade.php di resources/views/
5.Update navbar untuk menambahkan link ke dashboard
B. Cara Mengakses:
- Login terlebih dahulu di
/login - Setelah login, akan diarahkan ke dashboard
- Atau klik dropdown user → pilih "Dashboard"
- URL langsung:
/dashboard
C. Fitur yang Tersedia:
| Fitur | URL | Method | Keterangan |
|---|---|---|---|
| Dashboard utama | /dashboard |
GET | Halaman utama dashboard |
| Data AJAX | /dashboard/data |
GET | Data JSON untuk refresh |
| List produk | /products |
GET | Dari ProductController |
| Tambah produk | /products/create |
GET | Form tambah produk |
| Edit produk | /products/{id}/edit |
GET | Form edit produk |
D. Keamanan:
- Semua route dashboard dilindungi middleware
auth - Hanya user yang login bisa mengakses
- Data user ditampilkan di header dashboard
- Route login/logout berada di luar middleware
5. Testing Dashboard
Berikut Merupakan Hasil Dashboard yang telah di kembangkan:
# Test melalui browser:
1. http://localhost:8000/login # Login dulu
2. http://localhost:8000/dashboard # Dashboard utama
3. http://localhost:8000/dashboard/data # Data JSON
Dashboard siap digunakan dengan routing yang terstruktur, controller yang clean, dan tampilan yang informatif!

