Tutorial Laravel & ReactJS #9: Dashboard Nasabah – Total Saldo dan Mutasi

Tutorial #9: Dashboard Nasabah – Menampilkan Total Saldo dan Mutasi

Halo para nasabah setia! Hari ini kita akan membuat dashboard yang keren untuk nasabah melihat saldo dan mutasi. Kita akan pakai Bootstrap biar tabelnya rapi, SweetAlert untuk notifikasi, dan data langsung dari API yang sudah kita buat. Siap-siap jadi frontend developer handal! 

😂 Joke dashboard: "Kenapa dashboard bank warnanya hijau? Soalnya kalau merah, artinya saldo minus!" 🟢

Yang akan kita lakukan:

  • ✅ Mempercantik tampilan dashboard dengan komponen Bootstrap.
  • ✅ Menampilkan saldo dengan format Rupiah yang rapi.
  • ✅ Membuat tabel mutasi dengan strip dan hover effect.
  • ✅ Menambahkan indikator loading saat mengambil data.
  • ✅ Menangani error jika data gagal dimuat.
  • ✅ Menambahkan fitur refresh data (manual).
  • ✅ Membuat ringkasan transaksi (total setor, total tarik).

Langkah 1: Dashboard yang Sudah Ada, Kita Poles

Di tutorial #8, kita sudah membuat dashboard yang bisa menampilkan saldo dan mutasi. Sekarang kita akan memperbaikinya dengan:

  • Menambahkan card untuk informasi tambahan (no. rekening, total transaksi).
  • Membuat tabel lebih menarik dengan class Bootstrap table-striped table-hover.
  • Menampilkan loading spinner saat mengambil data.
  • Menambahkan tombol refresh untuk memuat ulang data.

Langkah 2: Memperbarui DashboardPage.js

Buka file src/pages/DashboardPage.js. Kita akan tulis ulang dengan fitur yang lebih lengkap.

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

function DashboardPage() {
  const [saldo, setSaldo] = useState(null);
  const [mutasi, setMutasi] = useState([]);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);

  // Fungsi untuk mengambil data dari API
  const fetchData = async () => {
    const token = localStorage.getItem('token');
    if (!token) return;

    setRefreshing(true);
    try {
      // Ambil saldo
      const saldoRes = await axios.get('http://127.0.0.1:8000/api/nasabah/saldo', {
        headers: { Authorization: `Bearer ${token}` }
      });
      setSaldo(saldoRes.data);

      // Ambil mutasi
      const mutasiRes = await axios.get('http://127.0.0.1:8000/api/nasabah/mutasi', {
        headers: { Authorization: `Bearer ${token}` }
      });
      setMutasi(mutasiRes.data.mutasi);
    } catch (error) {
      Swal.fire({
        icon: 'error',
        title: 'Gagal memuat data',
        text: error.response?.data?.message || 'Terjadi kesalahan'
      });
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  };

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

  // Hitung total setor dan tarik
  const totalSetor = mutasi
    .filter(trx => trx.jenis === 'Setor')
    .reduce((sum, trx) => sum + trx.jumlah, 0);
  const totalTarik = mutasi
    .filter(trx => trx.jenis === 'Tarik')
    .reduce((sum, trx) => sum + trx.jumlah, 0);

  // Fungsi untuk format Rupiah
  const formatRupiah = (angka) => {
    return new Intl.NumberFormat('id-ID', {
      style: 'currency',
      currency: 'IDR',
      minimumFractionDigits: 0
    }).format(angka);
  };

  if (loading) {
    return (
      <div className="text-center my-5">
        <div className="spinner-border text-primary" role="status">
          <span className="visually-hidden">Loading...</span>
        </div>
        <p className="mt-2">Mengambil data rekening...</p>
      </div>
    );
  }

  return (
    <div>
      {/* Header dengan tombol refresh */}
      <div className="d-flex justify-content-between align-items-center mb-4">
        <h2>Dashboard Nasabah</h2>
        <button
          className="btn btn-outline-primary"
          onClick={fetchData}
          disabled={refreshing}
        >
          {refreshing ? (
            <>
              <span className="spinner-border spinner-border-sm me-2" />
              Memuat...
            </>
          ) : (
            <>Refresh</>
          )}
        </button>
      </div>

      {/* Card Informasi Rekening */}
      <div className="row g-4 mb-4">
        <div className="col-md-6">
          <div className="card border-primary h-100">
            <div className="card-header bg-primary text-white">
              <h5 className="mb-0">Informasi Rekening</h5>
            </div>
            <div className="card-body">
              <h6 className="card-subtitle mb-2 text-muted">No. Rekening</h6>
              <p className="card-text fs-5">{saldo?.no_rekening}</p>
              <h6 className="card-subtitle mb-2 text-muted">Nama Nasabah</h6>
              <p className="card-text">{saldo?.nama}</p>
            </div>
          </div>
        </div>
        <div className="col-md-6">
          <div className="card border-success h-100">
            <div className="card-header bg-success text-white">
              <h5 className="mb-0">Ringkasan Transaksi</h5>
            </div>
            <div className="card-body">
              <h6 className="card-subtitle mb-2 text-muted">Total Setoran</h6>
              <p className="card-text text-success">{formatRupiah(totalSetor)}</p>
              <h6 className="card-subtitle mb-2 text-muted">Total Penarikan</h6>
              <p className="card-text text-danger">{formatRupiah(totalTarik)}</p>
            </div>
          </div>
        </div>
      </div>

      {/* Card Saldo Utama */}
      <div className="card mb-4 text-center bg-light">
        <div className="card-body">
          <h5 className="card-title">Total Saldo</h5>
          <p className="display-4 text-success fw-bold">
            {formatRupiah(saldo?.saldo)}
          </p>
        </div>
      </div>

      {/* Tabel Mutasi */}
      <h4 className="mb-3">Mutasi Rekening</h4>
      {mutasi.length === 0 ? (
        <div className="alert alert-info">
          Belum ada transaksi. Ayo segera menabung!
        </div>
      ) : (
        <div className="table-responsive">
          <table className="table table-striped table-hover">
            <thead className="table-primary">
              <tr>
                <th>Tanggal</th>
                <th>Jenis</th>
                <th>Jumlah</th>
                <th>Keterangan</th>
                <th>Petugas</th>
              </tr>
            </thead>
            <tbody>
              {mutasi.map((trx, index) => (
                <tr key={index}>
                  <td>{trx.tanggal}</td>
                  <td>
                    <span className={`badge ${trx.jenis === 'Setor' ? 'bg-success' : 'bg-danger'}`}>
                      {trx.jenis}
                    </span>
                  </td>
                  <td className={trx.jenis === 'Setor' ? 'text-success' : 'text-danger'}>
                    {formatRupiah(trx.jumlah)}
                  </td>
                  <td>{trx.keterangan}</td>
                  <td>{trx.petugas}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

export default DashboardPage;
😆 "Tabel mutasi itu kayak diary keuangan: catatan semua jajan dan tabungan. Kalau lupa, tinggal lihat!" 📔

Langkah 3: Menambahkan Styling Kustom (Opsional)

Kita bisa tambahkan sedikit CSS kustom di file src/index.css atau App.css untuk mempercantik. Misalnya:

.table thead th {
  background-color: #00796b;
  color: white;
}
.card {
  border-radius: 20px;
  box-shadow: 0 4px 8px rgba(0,0,0,0.05);
}
.display-4 {
  font-size: 3.5rem;
}

Tapi Bootstrap sudah cukup keren, jadi nggak wajib.

Langkah 4: Menambahkan Fitur Logout Otomatis Jika Token Kadaluarsa

Di method fetchData, kita bisa tambahkan penanganan jika response 401 (Unauthorized) yang berarti token sudah tidak valid. Maka kita hapus token dan redirect ke login.

Modifikasi bagian catch pada fetchData:

} catch (error) {
  if (error.response?.status === 401) {
    localStorage.removeItem('token');
    localStorage.removeItem('user');
    Swal.fire('Sesi habis', 'Silakan login ulang', 'warning')
      .then(() => window.location.href = '/login');
  } else {
    Swal.fire({
      icon: 'error',
      title: 'Gagal memuat data',
      text: error.response?.data?.message || 'Terjadi kesalahan'
    });
  }
}

Langkah 5: Uji Coba Dashboard

Pastikan backend Laravel berjalan dan frontend juga. Login sebagai nasabah, maka akan muncul dashboard seperti ini:

Hasil yang diharapkan:

  • Card informasi rekening (no. rekening, nama).
  • Card ringkasan transaksi (total setor, total tarik).
  • Saldo utama dengan angka besar.
  • Tabel mutasi dengan badge warna (hijau untuk setor, merah untuk tarik).
  • Tombol refresh untuk memuat ulang data.
  • Loading spinner saat pertama kali buka atau refresh.
💡 Catatan: Jika data mutasi masih kosong, coba buat transaksi dulu menggunakan endpoint deposit/withdraw via Postman, atau nanti di tutorial admin kita buat form untuk petugas.

Bonus: Menambahkan Grafik Sederhana dengan Chart.js

Kalau mau lebih keren, kita bisa tambahkan grafik batang untuk visualisasi transaksi per bulan. Install dulu:

npm install chart.js react-chartjs-2

Kemudian buat komponen grafik. Tapi karena ini tutorial dasar, kita skip dulu. Mungkin di artikel bonus.

Kesimpulan

  • ✅ Dashboard nasabah tampil cantik dengan Bootstrap.
  • ✅ Saldo dan mutasi diambil dari API real-time.
  • ✅ Ada ringkasan total setor dan tarik.
  • ✅ Loading state dan error handling sudah ditangani.
  • ✅ Tombol refresh memudahkan update data.

Di tutorial selanjutnya (#10: Notifikasi Interaktif dengan SweetAlert pada Aksi Nasabah) kita akan memperdalam penggunaan SweetAlert untuk konfirmasi logout, notifikasi error, dan lain-lain. Tapi sebenarnya kita sudah melakukannya di sini! Jadi tutorial #10 bisa fokus pada variasi SweetAlert atau fitur lain seperti ubah password. Sampai jumpa! 

Lebih baru Lebih lama

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