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 &lt; 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:
  1. Login terlebih dahulu di /login
  2. Setelah login, akan diarahkan ke dashboard
  3. Atau klik dropdown user → pilih "Dashboard"
  4. 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:
  1. Semua route dashboard dilindungi middleware auth
  2. Hanya user yang login bisa mengakses
  3. Data user ditampilkan di header dashboard
  4. 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!