Setup Layout & Dashboard Template

Setup Layout dan Dashboard Template

Di lesson ini, kita akan membuat layout dashboard yang profesional menggunakan Bootstrap 5 dengan sidebar yang modern.

Apa yang akan dibuat?

1. Master Layout: Sidebar fixed dengan menu navigasi
2. Dashboard: Statistik cards dengan update realtime
3. DashboardController: Logic untuk menghitung statistik
4. Toast Notification: Feedback system yang modern
5. Routes: Route untuk dashboard
Struktur Folder View yang Akan Dibuat:

resources/views/
├── layouts/
│   └── simrs.blade.php    ← Master Layout (dibuat di step ini)
├── dashboard.blade.php    ← Halaman dashboard
├── master/                ← CRUD Master Data (Section 2)
├── pasien/                ← Data Pasien (Section 3)
└── pendaftaran/           ← Pendaftaran (Section 4)

Step 1: Buat Folder layouts

Lokasi: File Explorer / VS Code
Aksi: [BUAT FOLDER BARU]
Cara:
  1. Buka folder project di VS Code
  2. Masuk ke folder resources/views/
  3. Klik kanan → New Folder → beri nama layouts

Step 2: Buat Master Layout

Lokasi File: resources/views/layouts/simrs.blade.php
Aksi: [BUAT FILE BARU]
Cara: Di folder layouts yang baru dibuat, klik kanan → New File → beri nama simrs.blade.php, lalu PASTE SEMUA kode di bawah ini:
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>@yield('title', 'SIMRS') - {{ config('app.name') }}</title>
    
    <!-- Bootstrap 5 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Bootstrap Icons -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
    <!-- DataTables CSS -->
    <link href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css" rel="stylesheet">
    
    <style>
        :root {
            --primary-color: #0d6efd;
            --sidebar-width: 260px;
        }
        body { background: #f4f6f9; }
        .sidebar {
            width: var(--sidebar-width);
            height: 100vh;
            background: linear-gradient(180deg, #1e3a5f 0%, #0d2137 100%);
            position: fixed;
            left: 0;
            top: 0;
            z-index: 1000;
            display: flex;
            flex-direction: column;
        }
        .sidebar .nav-link {
            color: rgba(255,255,255,0.7);
            padding: 12px 20px;
            border-radius: 8px;
            margin: 2px 10px;
        }
        .sidebar .nav-link:hover, .sidebar .nav-link.active {
            background: rgba(255,255,255,0.1);
            color: #fff;
        }
        .sidebar .nav-section {
            color: rgba(255,255,255,0.4);
            font-size: 11px;
            text-transform: uppercase;
            padding: 15px 20px 5px;
            letter-spacing: 1px;
        }
        .main-content {
            margin-left: var(--sidebar-width);
            padding: 20px;
        }
        .navbar-top {
            background: #fff;
            box-shadow: 0 2px 4px rgba(0,0,0,0.08);
        }
        .card { border: none; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .stat-card { border-left: 4px solid var(--primary-color); }
    </style>
    @stack('styles')
</head>
<body>
    <!-- Sidebar -->
    <nav class="sidebar d-flex flex-column">
        <div class="p-3 text-center border-bottom border-secondary">
            <h5 class="text-white mb-0"><i class="bi bi-hospital me-2"></i>SIMRS</h5>
            <small class="text-white-50">Sistem Informasi RS</small>
        </div>
        
        <div class="sidebar-menu py-3 flex-grow-1 overflow-auto">
            <ul class="nav flex-column">
                <!-- Dashboard -->
                <li class="nav-item">
                    <a href="{{ route('dashboard') }}" class="nav-link {{ request()->routeIs('dashboard') ? 'active' : '' }}">
                        <i class="bi bi-speedometer2 me-2"></i>Dashboard
                    </a>
                </li>
                
                <!-- Master Data Section -->
                <li class="nav-section">Master Data</li>
                <li class="nav-item">
                    <a href="/poliklinik" class="nav-link {{ request()->is('poliklinik*') ? 'active' : '' }}">
                        <i class="bi bi-building me-2"></i>Poliklinik
                    </a>
                </li>
                <li class="nav-item">
                    <a href="/dokter" class="nav-link {{ request()->is('dokter*') ? 'active' : '' }}">
                        <i class="bi bi-person-badge me-2"></i>Dokter
                    </a>
                </li>
                <li class="nav-item">
                    <a href="/jadwal-praktek" class="nav-link {{ request()->is('jadwal*') ? 'active' : '' }}">
                        <i class="bi bi-calendar-week me-2"></i>Jadwal Praktek
                    </a>
                </li>
                <li class="nav-item">
                    <a href="/obat" class="nav-link {{ request()->is('obat*') ? 'active' : '' }}">
                        <i class="bi bi-capsule me-2"></i>Obat
                    </a>
                </li>
                
                <!-- Pelayanan Section -->
                <li class="nav-section">Pelayanan</li>
                <li class="nav-item">
                    <a href="/pasien" class="nav-link {{ request()->is('pasien*') ? 'active' : '' }}">
                        <i class="bi bi-people me-2"></i>Pasien
                    </a>
                </li>
                <li class="nav-item">
                    <a href="/pendaftaran" class="nav-link {{ request()->is('pendaftaran*') ? 'active' : '' }}">
                        <i class="bi bi-clipboard-plus me-2"></i>Pendaftaran
                    </a>
                </li>
                <li class="nav-item">
                    <a href="/antrian" class="nav-link {{ request()->is('antrian*') ? 'active' : '' }}">
                        <i class="bi bi-ticket-perforated me-2"></i>Antrian
                    </a>
                </li>
            </ul>
        </div>
        
        <!-- User Info at Bottom -->
        <div class="p-3 border-top border-secondary">
            <div class="d-flex align-items-center text-white">
                <i class="bi bi-person-circle me-2" style="font-size: 1.5rem;"></i>
                <div class="flex-grow-1">
                    <small class="d-block">{{ auth()->user()->name ?? 'Guest' }}</small>
                    <small class="text-white-50">{{ auth()->user()->email ?? '' }}</small>
                </div>
            </div>
        </div>
    </nav>

    <!-- Main Content -->
    <div class="main-content">
        <!-- Top Navbar -->
        <nav class="navbar navbar-top rounded mb-4 px-3">
            <h5 class="mb-0">@yield('title', 'Dashboard')</h5>
            <div class="d-flex align-items-center gap-3">
                <span class="text-muted small">{{ now()->locale('id')->isoFormat('dddd, D MMMM Y') }}</span>
                <form method="POST" action="{{ route('logout') }}">
                    @csrf
                    <button type="submit" class="btn btn-outline-danger btn-sm">
                        <i class="bi bi-box-arrow-right me-1"></i>Logout
                    </button>
                </form>
            </div>
        </nav>
        
        <!-- Page Content -->
        @yield('content')
    </div>

    <!-- Toast Container -->
    <div class="toast-container position-fixed bottom-0 end-0 p-3">
        <div id="liveToast" class="toast" role="alert">
            <div class="toast-header">
                <i class="bi bi-bell me-2"></i>
                <strong class="me-auto">Notifikasi</strong>
                <button type="button" class="btn-close" data-bs-dismiss="toast"></button>
            </div>
            <div class="toast-body"></div>
        </div>
    </div>

    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <!-- Bootstrap 5 JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <!-- DataTables JS -->
    <script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
    
    <script>
    // Setup CSRF Token untuk semua AJAX request
    $.ajaxSetup({
        headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }
    });
    
    // Function untuk menampilkan Toast
    function showToast(message, type = 'success') {
        const toast = document.getElementById('liveToast');
        const toastBody = toast.querySelector('.toast-body');
        
        // Set warna berdasarkan type
        toast.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'bg-info', 'text-white');
        if (type === 'success') {
            toast.classList.add('bg-success', 'text-white');
        } else if (type === 'error') {
            toast.classList.add('bg-danger', 'text-white');
        } else if (type === 'warning') {
            toast.classList.add('bg-warning');
        } else {
            toast.classList.add('bg-info', 'text-white');
        }
        
        toastBody.textContent = message;
        const bsToast = new bootstrap.Toast(toast);
        bsToast.show();
    }
    
    // Flash message dari Laravel session
    @if(session('success'))
        showToast("{{ session('success') }}", 'success');
    @endif
    @if(session('error'))
        showToast("{{ session('error') }}", 'error');
    @endif
    @if(session('warning'))
        showToast("{{ session('warning') }}", 'warning');
    @endif
    </script>
    
    @stack('scripts')
</body>
</html>
Penjelasan Komponen Layout:

.sidebar Sidebar fixed di kiri dengan gradient biru
.main-content Area konten utama dengan margin-left untuk sidebar
@yield('content') Tempat konten halaman di-inject
showToast() Function JS untuk notifikasi popup
$.ajaxSetup() Set CSRF token untuk semua AJAX request

Step 3: Buat File Dashboard View

Lokasi File: resources/views/dashboard.blade.php
Aksi: [BUAT FILE BARU]
Cara: Di folder resources/views/, buat file baru bernama dashboard.blade.php, lalu PASTE SEMUA kode di bawah ini:
@extends('layouts.simrs')

@section('title', 'Dashboard')

@section('content')
<!-- Stats Cards -->
<div class="row g-3 mb-4">
    <div class="col-md-3">
        <div class="card stat-card">
            <div class="card-body">
                <div class="d-flex justify-content-between">
                    <div>
                        <h6 class="text-muted mb-1">Pasien Hari Ini</h6>
                        <h3 class="mb-0" id="stat-pasien">{{ $stats['pasien_hari_ini'] ?? 0 }}</h3>
                    </div>
                    <div class="text-primary opacity-50">
                        <i class="bi bi-people" style="font-size: 2.5rem;"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card stat-card" style="border-left-color: #198754;">
            <div class="card-body">
                <div class="d-flex justify-content-between">
                    <div>
                        <h6 class="text-muted mb-1">Antrian Menunggu</h6>
                        <h3 class="mb-0" id="stat-antrian">{{ $stats['antrian_menunggu'] ?? 0 }}</h3>
                    </div>
                    <div class="text-success opacity-50">
                        <i class="bi bi-hourglass-split" style="font-size: 2.5rem;"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card stat-card" style="border-left-color: #ffc107;">
            <div class="card-body">
                <div class="d-flex justify-content-between">
                    <div>
                        <h6 class="text-muted mb-1">Resep Pending</h6>
                        <h3 class="mb-0" id="stat-resep">{{ $stats['resep_pending'] ?? 0 }}</h3>
                    </div>
                    <div class="text-warning opacity-50">
                        <i class="bi bi-prescription2" style="font-size: 2.5rem;"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card stat-card" style="border-left-color: #0dcaf0;">
            <div class="card-body">
                <div class="d-flex justify-content-between">
                    <div>
                        <h6 class="text-muted mb-1">Pendapatan Hari Ini</h6>
                        <h5 class="mb-0" id="stat-pendapatan">{{ format_rupiah($stats['pendapatan'] ?? 0) }}</h5>
                    </div>
                    <div class="text-info opacity-50">
                        <i class="bi bi-cash-coin" style="font-size: 2.5rem;"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Info Cards -->
<div class="row g-3">
    <div class="col-md-8">
        <div class="card">
            <div class="card-header bg-white">
                <h6 class="mb-0"><i class="bi bi-clock-history me-2"></i>Antrian Saat Ini</h6>
            </div>
            <div class="card-body">
                <div class="table-responsive">
                    <table class="table table-hover mb-0">
                        <thead class="table-light">
                            <tr>
                                <th>No. Antrian</th>
                                <th>Nama Pasien</th>
                                <th>Poliklinik</th>
                                <th>Status</th>
                            </tr>
                        </thead>
                        <tbody>
                            @forelse($antrians ?? [] as $antrian)
                            <tr>
                                <td><span class="badge bg-primary">{{ $antrian->no_antrian }}</span></td>
                                <td>{{ $antrian->kunjungan->pasien->nama ?? '-' }}</td>
                                <td>{{ $antrian->poliklinik->nama ?? '-' }}</td>
                                <td>
                                    @if($antrian->status == 'dipanggil')
                                        <span class="badge bg-success">Dipanggil</span>
                                    @else
                                        <span class="badge bg-warning">Menunggu</span>
                                    @endif
                                </td>
                            </tr>
                            @empty
                            <tr>
                                <td colspan="4" class="text-center text-muted py-3">
                                    Tidak ada antrian saat ini
                                </td>
                            </tr>
                            @endforelse
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="card">
            <div class="card-header bg-white">
                <h6 class="mb-0"><i class="bi bi-prescription2 me-2"></i>Resep Terbaru</h6>
            </div>
            <div class="card-body p-0">
                <ul class="list-group list-group-flush">
                    @forelse($reseps ?? [] as $resep)
                    <li class="list-group-item d-flex justify-content-between align-items-center">
                        <div>
                            <strong>{{ $resep->rekamMedis->kunjungan->pasien->nama ?? '-' }}</strong>
                            <br><small class="text-muted">{{ $resep->dokter->nama ?? '-' }}</small>
                        </div>
                        <span class="badge bg-{{ $resep->status == 'menunggu' ? 'warning' : 'info' }}">
                            {{ ucfirst($resep->status) }}
                        </span>
                    </li>
                    @empty
                    <li class="list-group-item text-center text-muted py-3">
                        Tidak ada resep pending
                    </li>
                    @endforelse
                </ul>
            </div>
        </div>
    </div>
</div>
@endsection

@push('scripts')
<script>
// AJAX Polling untuk update realtime setiap 10 detik
function refreshDashboard() {
    $.get('{{ route("dashboard.stats") }}', function(data) {
        $('#stat-pasien').text(data.pasien_hari_ini);
        $('#stat-antrian').text(data.antrian_menunggu);
        $('#stat-resep').text(data.resep_pending);
        $('#stat-pendapatan').text('Rp ' + new Intl.NumberFormat('id-ID').format(data.pendapatan));
    });
}

// Refresh setiap 10 detik (10000ms)
setInterval(refreshDashboard, 10000);
</script>
@endpush

Step 4: Buat DashboardController

Lokasi: Terminal / Command Prompt
Aksi: [JALANKAN COMMAND]
Cara: Buka terminal di folder project, jalankan command berikut:
# Buat controller untuk Dashboard
php artisan make:controller DashboardController
Lokasi File: app/Http/Controllers/DashboardController.php
Aksi: [UPDATE FILE]
Cara: Buka file yang baru dibuat, HAPUS SEMUA isinya, lalu PASTE kode di bawah ini:
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class DashboardController extends Controller
{
    /**
     * Tampilkan halaman dashboard dengan statistik
     */
    public function index()
    {
        // Statistik (akan diisi dengan data real di section berikutnya)
        $stats = [
            'pasien_hari_ini' => 0,
            'antrian_menunggu' => 0,
            'resep_pending' => 0,
            'pendapatan' => 0,
        ];

        // Data antrian (kosong dulu)
        $antrians = collect([]);

        // Data resep pending (kosong dulu)
        $reseps = collect([]);

        return view('dashboard', compact('stats', 'antrians', 'reseps'));
    }

    /**
     * API endpoint untuk AJAX polling statistik
     * Dipanggil setiap 10 detik oleh JavaScript
     */
    public function stats()
    {
        // Return statistik dalam format JSON
        return response()->json([
            'pasien_hari_ini' => 0,
            'antrian_menunggu' => 0,
            'resep_pending' => 0,
            'pendapatan' => 0,
        ]);
    }
}
Catatan:
Di tahap ini, statistik masih 0 semua karena kita belum punya tabel dan data.
Statistik akan diupdate di section berikutnya setelah tabel-tabel dibuat.

Step 5: Tambahkan Routes Dashboard

Lokasi File: routes/web.php
Aksi: [UPDATE FILE]
Cara: Buka file routes/web.php, cari route dashboard yang existing, ubah menjadi seperti di bawah:
<?php

use App\Http\Controllers\DashboardController;
use Illuminate\Support\Facades\Route;

// ===== SECTION 1: SETUP =====

Route::get('/', function () {
    return redirect()->route('dashboard');
});

Route::middleware(['auth'])->group(function () {
    // Dashboard
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
    Route::get('/dashboard/stats', [DashboardController::class, 'stats'])->name('dashboard.stats');
});

// ===== END SECTION 1 =====

require __DIR__.'/auth.php';
Penjelasan Routes:

Route::get('/') Redirect ke dashboard (halaman utama)
/dashboard Halaman dashboard dengan statistik
/dashboard/stats API endpoint untuk AJAX polling
middleware(['auth']) Hanya user yang sudah login yang bisa akses

Full Source Code

Files yang Dibuat (4 files):
Kode sudah lengkap di masing-masing Step.

1 resources/views/layouts/simrs.blade.php [BUAT FILE BARU] Step 2
2 resources/views/dashboard.blade.php [BUAT FILE BARU] Step 3
3 app/Http/Controllers/DashboardController.php [BUAT FILE BARU] Step 4
4 routes/web.php [UPDATE FILE] Step 5

Checkpoint - Test Manual

Langkah Testing:

1. Pastikan Server Berjalan:
php artisan serve
Server harus berjalan di http://localhost:8000

2. Login ke Sistem:
- Buka browser: http://localhost:8000/login
- Login dengan akun yang sudah didaftarkan

3. Cek Dashboard:
- Setelah login, Anda akan di-redirect ke dashboard
- Pastikan sidebar muncul di kiri
- Pastikan 4 cards statistik muncul (nilainya masih 0)
- Pastikan tabel antrian dan resep muncul (kosong)

4. Test Toast Notification:
- Buka Console browser (F12 → Console)
- Ketik: showToast('Test berhasil!', 'success')
- Toast hijau harus muncul di kanan bawah

5. Test AJAX Polling:
- Buka Network tab di browser DevTools (F12 → Network)
- Tunggu 10 detik
- Harus ada request ke /dashboard/stats setiap 10 detik
Troubleshooting Error Umum:

Error: "Route [dashboard] not defined"
Solusi: Pastikan routes sudah ditambahkan di routes/web.php

Error: Sidebar tidak muncul
Solusi: Pastikan file layout sudah ada di resources/views/layouts/simrs.blade.php

Error: "format_rupiah not found"
Solusi: Pastikan Helper sudah terdaftar di composer.json dan jalankan composer dump-autoload

Ringkasan

Yang Sudah Dibuat:

1. Master Layout (simrs.blade.php):
- Sidebar fixed dengan menu navigasi
- Top navbar dengan tanggal dan logout
- Toast notification system
- AJAX CSRF setup otomatis

2. Dashboard (dashboard.blade.php):
- 4 statistik cards (pasien, antrian, resep, pendapatan)
- Tabel antrian saat ini
- Daftar resep pending
- AJAX polling untuk update realtime

3. DashboardController:
- Method index() untuk tampilkan halaman
- Method stats() untuk API polling

4. Routes:
- /dashboard (halaman utama)
- /dashboard/stats (API polling)
Lesson Berikutnya:
Di Lesson 5: Implementasi Role dan Permission, kita akan:
- Install package Spatie Permission
- Membuat role (Admin, Dokter, Perawat, Apoteker, Kasir)
- Membuat middleware untuk cek akses

Dengan role & permission, setiap user hanya bisa akses menu sesuai haknya.