Tutorial Laravel & ReactJS #13: Halaman Login Multi-Level (Admin & Petugas) di React

Tutorial #13: Halaman Login Multi-Level (Admin & Petugas) di React

Halo para pengelola bank! Setelah berhasil membuat API untuk admin dan petugas di tutorial #4–#6, sekarang saatnya kita membangun antarmuka login dan dashboard mereka di React. Mereka akan login, dan kita akan arahkan ke halaman yang sesuai. Siap jadi resepsionis bank digital? 

😂 Joke resepsionis: "Kenapa admin dan petugas punya pintu masuk sendiri? Biar nggak rebutan sama nasabah yang mau nabung!" 🚪

Yang akan kita lakukan:

  • ✅ Menggunakan halaman login yang sama (sudah ada dari tutorial #8) untuk semua role.
  • ✅ Menyimpan token dan data user setelah login (role ikut tersimpan).
  • ✅ Redirect ke dashboard yang sesuai (admin atau petugas).
  • ✅ Membuat komponen dashboard sederhana untuk admin dan petugas.
  • ✅ Menambahkan proteksi route berdasarkan role (private route).
  • ✅ Menampilkan notifikasi SweetAlert untuk sukses/gagal login.

Langkah 1: Struktur Routing untuk Admin & Petugas

Buka src/App.js. Tambahkan rute baru untuk dashboard admin dan petugas, serta gunakan komponen PrivateRoute yang sudah dimodifikasi untuk mengecek role.

import AdminDashboard from './pages/AdminDashboard';
import PetugasDashboard from './pages/PetugasDashboard';

// ... di dalam Routes
<Route path="/login" element={<LoginPage />} />
<Route path="/dashboard" element={
  <PrivateRoute allowedRoles={['nasabah']}>
    <DashboardPage />
  </PrivateRoute>
} />
<Route path="/admin/dashboard" element={
  <PrivateRoute allowedRoles={['admin']}>
    <AdminDashboard />
  </PrivateRoute>
} />
<Route path="/petugas/dashboard" element={
  <PrivateRoute allowedRoles={['petugas']}>
    <PetugasDashboard />
  </PrivateRoute>
} />

Langkah 2: PrivateRoute dengan Pengecekan Role

Buat atau perbarui file src/components/PrivateRoute.js:

import React from 'react';
import { Navigate } from 'react-router-dom';

function PrivateRoute({ children, allowedRoles }) {
  const token = localStorage.getItem('token');
  const user = JSON.parse(localStorage.getItem('user') || '{}');

  if (!token) {
    return <Navigate to="/login" />;
  }

  if (allowedRoles && !allowedRoles.includes(user.role)) {
    // Redirect ke dashboard yang sesuai jika role tidak diizinkan
    if (user.role === 'admin') return <Navigate to="/admin/dashboard" />;
    if (user.role === 'petugas') return <Navigate to="/petugas/dashboard" />;
    if (user.role === 'nasabah') return <Navigate to="/dashboard" />;
    return <Navigate to="/" />;
  }

  return children;
}

export default PrivateRoute;

Langkah 3: Modifikasi Halaman Login (sudah ada)

Di src/pages/LoginPage.js, kita hanya perlu menambahkan logika redirect setelah login sukses berdasarkan role. Kode yang sudah ada (dari tutorial #8) bisa ditambahkan bagian berikut di dalam blok try setelah menyimpan token:

// Redirect berdasarkan role
if (user.role === 'admin') {
  navigate('/admin/dashboard');
} else if (user.role === 'petugas') {
  navigate('/petugas/dashboard');
} else {
  navigate('/dashboard'); // nasabah
}

Pastikan juga di response login dari API, kita mengirimkan role user (seperti yang sudah dilakukan di tutorial #3).

Langkah 4: Membuat Dashboard Admin Sederhana

Buat file src/pages/AdminDashboard.js:

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import Swal from 'sweetalert2';

function AdminDashboard() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUsers();
  }, []);

  const fetchUsers = async () => {
    const token = localStorage.getItem('token');
    try {
      const response = await axios.get('http://127.0.0.1:8000/api/users', {
        headers: { Authorization: `Bearer ${token}` }
      });
      setUsers(response.data);
    } catch (error) {
      Swal.fire('Error', 'Gagal memuat data pengguna', 'error');
    } finally {
      setLoading(false);
    }
  };

  // Hitung jumlah berdasarkan role
  const adminCount = users.filter(u => u.role.name === 'Admin').length;
  const petugasCount = users.filter(u => u.role.name === 'Petugas').length;
  const nasabahCount = users.filter(u => u.role.name === 'Nasabah').length;

  return (
    <div className="container mt-4">
      <h2>Dashboard Admin</h2>
      <div className="row">
        <div className="col-md-3 mb-3">
          <div className="card text-white bg-primary">
            <div className="card-body">
              <h5 className="card-title">Total Pengguna</h5>
              <p className="card-text display-6">{users.length}</p>
            </div>
          </div>
        </div>
        <div className="col-md-3 mb-3">
          <div className="card text-white bg-danger">
            <div className="card-body">
              <h5 className="card-title">Admin</h5>
              <p className="card-text display-6">{adminCount}</p>
            </div>
          </div>
        </div>
        <div className="col-md-3 mb-3">
          <div className="card text-white bg-success">
            <div className="card-body">
              <h5 className="card-title">Petugas</h5>
              <p className="card-text display-6">{petugasCount}</p>
            </div>
          </div>
        </div>
        <div className="col-md-3 mb-3">
          <div className="card text-white bg-warning">
            <div className="card-body">
              <h5 className="card-title">Nasabah</h5>
              <p className="card-text display-6">{nasabahCount}</p>
            </div>
          </div>
        </div>
      </div>

      <div className="card mt-4">
        <div className="card-header bg-primary text-white">
          <h5 className="mb-0">Manajemen Pengguna (Admin & Petugas)</h5>
        </div>
        <div className="card-body">
          <table className="table table-striped">
            <thead>
              <tr>
                <th>ID</th>
                <th>Nama</th>
                <th>Email</th>
                <th>Role</th>
                <th>Aksi</th>
              </tr>
            </thead>
            <tbody>
              {users.filter(u => u.role.name !== 'Nasabah').map(user => (
                <tr key={user.id}>
                  <td>{user.id}</td>
                  <td>{user.name}</td>
                  <td>{user.email}</td>
                  <td>
                    <span className={`badge ${user.role.name === 'Admin' ? 'bg-danger' : 'bg-success'}`}>
                      {user.role.name}
                    </span>
                  </td>
                  <td>
                    <button className="btn btn-sm btn-warning me-2">Edit</button>
                    <button className="btn btn-sm btn-danger">Hapus</button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

export default AdminDashboard;

Langkah 5: Dashboard Petugas Sederhana

Buat file src/pages/PetugasDashboard.js:

import React, { useState } from 'react';
import axios from 'axios';
import Swal from 'sweetalert2';

function PetugasDashboard() {
  const [form, setForm] = useState({
    account_id: '',
    amount: '',
    description: ''
  });
  const [loading, setLoading] = useState(false);

  const handleChange = (e) => {
    setForm({ ...form, [e.target.name]: e.target.value });
  };

  const handleDeposit = async (e) => {
    e.preventDefault();
    setLoading(true);
    const token = localStorage.getItem('token');
    try {
      await axios.post('http://127.0.0.1:8000/api/deposit', form, {
        headers: { Authorization: `Bearer ${token}` }
      });
      Swal.fire('Sukses', 'Setor tunai berhasil', 'success');
      setForm({ account_id: '', amount: '', description: '' });
    } catch (error) {
      Swal.fire('Gagal', error.response?.data?.message || 'Terjadi kesalahan', 'error');
    } finally {
      setLoading(false);
    }
  };

  const handleWithdraw = async (e) => {
    e.preventDefault();
    setLoading(true);
    const token = localStorage.getItem('token');
    try {
      await axios.post('http://127.0.0.1:8000/api/withdraw', form, {
        headers: { Authorization: `Bearer ${token}` }
      });
      Swal.fire('Sukses', 'Tarik tunai berhasil', 'success');
      setForm({ account_id: '', amount: '', description: '' });
    } catch (error) {
      Swal.fire('Gagal', error.response?.data?.message || 'Terjadi kesalahan', 'error');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="container mt-4">
      <h2>Dashboard Petugas</h2>
      <div className="row">
        <div className="col-md-6">
          <div className="card">
            <div className="card-header bg-success text-white">
              <h5 className="mb-0">Form Setor Tunai</h5>
            </div>
            <div className="card-body">
              <form onSubmit={handleDeposit}>
                <div className="mb-3">
                  <label className="form-label">ID Rekening</label>
                  <input type="number" className="form-control" name="account_id" value={form.account_id} onChange={handleChange} required />
                </div>
                <div className="mb-3">
                  <label className="form-label">Jumlah (Rp)</label>
                  <input type="number" className="form-control" name="amount" value={form.amount} onChange={handleChange} required />
                </div>
                <div className="mb-3">
                  <label className="form-label">Keterangan</label>
                  <input type="text" className="form-control" name="description" value={form.description} onChange={handleChange} />
                </div>
                <button type="submit" className="btn btn-success w-100" disabled={loading}>
                  {loading ? 'Memproses...' : 'Setor'}
                </button>
              </form>
            </div>
          </div>
        </div>
        <div className="col-md-6">
          <div className="card">
            <div className="card-header bg-danger text-white">
              <h5 className="mb-0">Form Tarik Tunai</h5>
            </div>
            <div className="card-body">
              <form onSubmit={handleWithdraw}>
                <div className="mb-3">
                  <label className="form-label">ID Rekening</label>
                  <input type="number" className="form-control" name="account_id" value={form.account_id} onChange={handleChange} required />
                </div>
                <div className="mb-3">
                  <label className="form-label">Jumlah (Rp)</label>
                  <input type="number" className="form-control" name="amount" value={form.amount} onChange={handleChange} required />
                </div>
                <div className="mb-3">
                  <label className="form-label">Keterangan</label>
                  <input type="text" className="form-control" name="description" value={form.description} onChange={handleChange} />
                </div>
                <button type="submit" className="btn btn-danger w-100" disabled={loading}>
                  {loading ? 'Memproses...' : 'Tarik'}
                </button>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default PetugasDashboard;

Langkah 6: Menyesuaikan Navigasi (Navbar)

Update src/components/Navbar.js agar menampilkan menu yang sesuai dengan role pengguna yang login.

// Di dalam komponen Navbar
import { Link, useNavigate } from 'react-router-dom';
import Swal from 'sweetalert2';

function Navbar() {
  const navigate = useNavigate();
  const token = localStorage.getItem('token');
  const user = JSON.parse(localStorage.getItem('user') || '{}');

  const handleLogout = () => {
    Swal.fire({
      title: 'Yakin mau logout?',
      icon: 'question',
      showCancelButton: true,
      confirmButtonText: 'Ya, logout',
      cancelButtonText: 'Batal'
    }).then((result) => {
      if (result.isConfirmed) {
        localStorage.removeItem('token');
        localStorage.removeItem('user');
        navigate('/login');
        Swal.fire('Berhasil logout!', '', 'success');
      }
    });
  };

  return (
    <nav className="navbar navbar-expand-lg navbar-dark bg-primary">
      <div className="container">
        <Link className="navbar-brand" to="/">🏦 Bank Tabungan</Link>
        <button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
          <span className="navbar-toggler-icon"></span>
        </button>
        <div className="collapse navbar-collapse" id="navbarNav">
          <ul className="navbar-nav ms-auto">
            <li className="nav-item">
              <Link className="nav-link" to="/">Home</Link>
            </li>
            {!token ? (
              <li className="nav-item">
                <Link className="nav-link" to="/login">Login</Link>
              </li>
            ) : (
              <>
                {user.role === 'nasabah' && (
                  <li className="nav-item">
                    <Link className="nav-link" to="/dashboard">Dashboard</Link>
                  </li>
                )}
                {user.role === 'admin' && (
                  <li className="nav-item">
                    <Link className="nav-link" to="/admin/dashboard">Admin Dashboard</Link>
                  </li>
                )}
                {user.role === 'petugas' && (
                  <li className="nav-item">
                    <Link className="nav-link" to="/petugas/dashboard">Petugas Dashboard</Link>
                  </li>
                )}
                <li className="nav-item">
                  <span className="nav-link">Halo, {user.name}</span>
                </li>
                <li className="nav-item">
                  <button className="btn btn-outline-light btn-sm" onClick={handleLogout}>Logout</button>
                </li>
              </>
            )}
          </ul>
        </div>
      </div>
    </nav>
  );
}

export default Navbar;
😆 "Petugas bank sekarang bisa setor dan tarik tunai lewat web, nggak perlu pakai mesin hitung manual!" 💵

Langkah 7: Uji Coba Login Multi-Level

  1. Login sebagai admin (email: admin@bank.com, password: rahasia123) → harusnya redirect ke /admin/dashboard.
  2. Login sebagai petugas (email: petugas@bank.com, password: rahasia123) → redirect ke /petugas/dashboard.
  3. Login sebagai nasabah (budi@mail.com) → redirect ke /dashboard (yang sudah dibuat di tutorial #9).
  4. Coba akses langsung /admin/dashboard tanpa token → redirect ke login.
  5. Coba akses /admin/dashboard dengan token nasabah → redirect ke dashboard nasabah (karena tidak punya akses).
💡 Catatan: Pastikan endpoint API yang digunakan sesuai dengan yang sudah dibuat di tutorial #4, #5, #6. Jika ada error CORS, atur di backend atau gunakan proxy di React (seperti yang dijelaskan di tutorial #8).

Kesimpulan

  • ✅ Admin dan petugas bisa login menggunakan halaman yang sama, dan diarahkan ke dashboard masing-masing.
  • ✅ Proteksi route berdasarkan role bekerja dengan baik.
  • ✅ Dashboard admin menampilkan statistik dan tabel pengguna (fitur CRUD akan kita lanjutkan di tutorial berikutnya).
  • ✅ Dashboard petugas sudah memiliki form untuk setor dan tarik tunai.

Di tutorial selanjutnya (#14: Dashboard Admin – Manajemen Pengguna (CRUD) dengan React) kita akan membuat fitur tambah, edit, dan hapus pengguna dari dashboard admin. Sampai jumpa!


Daftar Tutorial Lanjutan (Rencana)

  • Tutorial #14: Dashboard Admin – Manajemen Pengguna (CRUD) dengan React
  • Tutorial #15: Dashboard Petugas – Melihat Mutasi Nasabah
  • Tutorial #16: Fitur Ubah Password untuk Semua Level Pengguna
  • Tutorial #17: Menambahkan Fitur Profil dan Edit Profil
  • Tutorial #18: Laporan Transaksi (Admin) – Filter dan Export ke Excel/PDF
  • Tutorial #19: Finalisasi dan Penyempurnaan Aplikasi
Lebih baru Lebih lama

نموذج الاتصال