Membuat Dashboard Admin dengan Statistik dan Grafik

Irez Abdullah
Irez Abdullah
27 Okt 2025•2090 dibaca
Membuat Dashboard Admin dengan Statistik dan Grafik

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

  1. Copy kode controller di atas ke file tersebut
  2. **Update routes/web.php** dengan routing yang sudah disediakan
  3. **Buat file view dashboard.blade.php** di resources/views/
  4. 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!