Tutorial Laravel & ReactJS #19: Laporan Transaksi (Admin) – Filter & Export

Tutorial #19: Laporan Transaksi (Admin) – Filter dan Export ke Excel/PDF

Halo para admin super! Hari ini kita akan membuat halaman laporan transaksi yang canggih. Admin bisa melihat semua transaksi (setor & tarik) yang terjadi, memfilter berdasarkan rentang tanggal, akun tertentu, dan bahkan mengekspor ke file Excel atau PDF. Siap jadi analis data?

😂 Joke admin: "Kenapa laporan transaksi harus difilter? Biar nggak pusing lihat data segunung!" 🏔️

Yang akan kita lakukan:

  • ✅ Membuat endpoint API di Laravel untuk mengambil data transaksi dengan filter (tanggal, akun).
  • ✅ Membuat halaman baru di React: AdminReports.js.
  • ✅ Menambahkan form filter dengan input tanggal (dari & sampai) dan pilihan akun.
  • ✅ Menampilkan data transaksi dalam tabel Bootstrap yang rapi.
  • ✅ Menambahkan tombol export ke Excel (menggunakan xlsx) dan PDF (menggunakan jspdf).
  • ✅ Menambahkan proteksi route hanya untuk admin.

Langkah 1: Membuat Endpoint API di Laravel dengan Filter

Kita perlu endpoint yang bisa menerima parameter filter. Buka routes/api.php dan tambahkan route di dalam group middleware isAdmin:

Route::middleware(['auth:sanctum', 'isAdmin'])->group(function () {
    Route::get('/admin/transactions', [App\Http\Controllers\Api\AdminController::class, 'transactions']);
});

Buat controller baru atau gunakan yang sudah ada. Misal buat AdminController:

php artisan make:controller Api/AdminController

Isi method transactions:

public function transactions(Request $request)
{
    $query = Transaction::with(['account.user', 'petugas']);

    // Filter berdasarkan tanggal (from_date dan to_date)
    if ($request->has('from_date')) {
        $query->whereDate('created_at', '>=', $request->from_date);
    }
    if ($request->has('to_date')) {
        $query->whereDate('created_at','<=', $request->to_date);
          
    }

    // Filter berdasarkan account_id
    if ($request->has('account_id')) {
        $query->where('account_id', $request->account_id);
    }

    // Urutkan descending (terbaru)
    $transactions = $query->orderBy('created_at', 'desc')->get();

    // Format data untuk frontend
    $data = $transactions->map(function ($trx) {
        return [
            'id' => $trx->id,
            'tanggal' => $trx->created_at->format('d-m-Y H:i'),
            'nasabah' => $trx->account->user->name ?? 'Unknown',
            'no_rekening' => $trx->account->account_number ?? 'Unknown',
            'jenis' => $trx->type == 'debit' ? 'Setor' : 'Tarik',
            'jumlah' => $trx->amount,
            'keterangan' => $trx->description,
            'petugas' => $trx->petugas->name ?? 'Unknown',
        ];
    });

    return response()->json($data);
}

Jangan lupa import model Transaction di atas.

💡 Catatan: Endpoint ini hanya bisa diakses oleh admin karena sudah diberi middleware isAdmin. Filter menggunakan query string, misal: /api/admin/transactions?from_date=2025-01-01&to_date=2025-01-31&account_id=1.

Langkah 2: Install Library untuk Export di React

Buka terminal di folder frontend dan install dua package ini:

npm install xlsx jspdf jspdf-autotable
  • xlsx untuk export ke Excel.
  • jspdf dan jspdf-autotable untuk export ke PDF dengan tabel.

📁 Langkah 3: Membuat Halaman AdminReports.js

Buat file src/pages/AdminReports.js. Kita akan buat komponen dengan state untuk filter, data, dan loading.

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Swal from 'sweetalert2';
import * as XLSX from 'xlsx';
import jsPDF from 'jspdf';
import 'jspdf-autotable';

function AdminReports() {
  const [transactions, setTransactions] = useState([]);
  const [loading, setLoading] = useState(false);
  const [filters, setFilters] = useState({
    from_date: '',
    to_date: '',
    account_id: ''
  });
  const [accounts, setAccounts] = useState([]); // untuk dropdown pilihan akun

  // Ambil daftar akun untuk dropdown (opsional)
  useEffect(() => {
    fetchAccounts();
  }, []);

  const fetchAccounts = async () => {
    const token = localStorage.getItem('token');
    try {
      const response = await axios.get('http://127.0.0.1:8000/api/accounts', {
        headers: { Authorization: `Bearer ${token}` }
      });
      setAccounts(response.data);
    } catch (error) {
      console.error('Gagal ambil akun');
    }
  };

  const fetchTransactions = async () => {
    setLoading(true);
    const token = localStorage.getItem('token');
    try {
      const params = new URLSearchParams();
      if (filters.from_date) params.append('from_date', filters.from_date);
      if (filters.to_date) params.append('to_date', filters.to_date);
      if (filters.account_id) params.append('account_id', filters.account_id);

      const response = await axios.get(`http://127.0.0.1:8000/api/admin/transactions?${params}`, {
        headers: { Authorization: `Bearer ${token}` }
      });
      setTransactions(response.data);
    } catch (error) {
      Swal.fire('Error', 'Gagal mengambil data transaksi', 'error');
    } finally {
      setLoading(false);
    }
  };

  const handleFilterChange = (e) => {
    setFilters({ ...filters, [e.target.name]: e.target.value });
  };

  const handleSearch = (e) => {
    e.preventDefault();
    fetchTransactions();
  };

  const resetFilters = () => {
    setFilters({ from_date: '', to_date: '', account_id: '' });
    fetchTransactions(); // ambil semua
  };

  // Export ke Excel
  const exportToExcel = () => {
    const ws = XLSX.utils.json_to_sheet(transactions);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Transaksi');
    XLSX.writeFile(wb, 'laporan_transaksi.xlsx');
  };

  // Export ke PDF
  const exportToPDF = () => {
    const doc = new jsPDF();
    doc.text('Laporan Transaksi', 14, 10);
    const tableColumn = ['Tanggal', 'Nasabah', 'No. Rekening', 'Jenis', 'Jumlah', 'Keterangan', 'Petugas'];
    const tableRows = transactions.map(trx => [
      trx.tanggal,
      trx.nasabah,
      trx.no_rekening,
      trx.jenis,
      `Rp ${trx.jumlah.toLocaleString()}`,
      trx.keterangan,
      trx.petugas
    ]);
    doc.autoTable({
      head: [tableColumn],
      body: tableRows,
      startY: 20,
    });
    doc.save('laporan_transaksi.pdf');
  };

Langkah 4: Membuat Tampilan Form Filter dan Tabel

Tambahkan JSX di dalam return:

return (
  <div className="container mt-4">
    <h2>Laporan Transaksi</h2>
    <div className="card mb-4">
      <div className="card-header bg-primary text-white">
        <h5 className="mb-0">Filter Transaksi</h5>
      </div>
      <div className="card-body">
        <form onSubmit={handleSearch}>
          <div className="row">
            <div className="col-md-3 mb-3">
              <label className="form-label">Dari Tanggal</label>
              <input
                type="date"
                className="form-control"
                name="from_date"
                value={filters.from_date}
                onChange={handleFilterChange}
              />
            </div>
            <div className="col-md-3 mb-3">
              <label className="form-label">Sampai Tanggal</label>
              <input
                type="date"
                className="form-control"
                name="to_date"
                value={filters.to_date}
                onChange={handleFilterChange}
              />
            </div>
            <div className="col-md-3 mb-3">
              <label className="form-label">Pilih Akun</label>
              <select className="form-select" name="account_id" value={filters.account_id} onChange={handleFilterChange}>
                <option value="">Semua Akun</option>
                {accounts.map(acc => (
                  <option key={acc.id} value={acc.id}>{acc.account_number} - {acc.user?.name}</option>
                ))}
              </select>
            </div>
            <div className="col-md-3 mb-3 d-flex align-items-end">
              <button type="submit" className="btn btn-primary me-2">Cari</button>
              <button type="button" className="btn btn-secondary" onClick={resetFilters}>Reset</button>
            </div>
          </div>
        </form>
      </div>
    </div>

    <div className="export-buttons">
      <button className="btn btn-success" onClick={exportToExcel}>📊 Export Excel</button>
      <button className="btn btn-danger" onClick={exportToPDF}>📄 Export PDF</button>
    </div>

    {loading ? (
      <div className="text-center">
        <div className="spinner-border text-primary" role="status">
          <span className="visually-hidden">Loading...</span>
        </div>
      </div>
    ) : (
      <div className="table-responsive">
        <table className="table table-striped table-hover">
          <thead>
            <tr>
              <th>Tanggal</th>
              <th>Nasabah</th>
              <th>No. Rekening</th>
              <th>Jenis</th>
              <th>Jumlah</th>
              <th>Keterangan</th>
              <th>Petugas</th>
            </tr>
          </thead>
          <tbody>
            {transactions.length > 0 ? (
              transactions.map((trx, idx) => (
                <tr key={idx}>
                  <td>{trx.tanggal}</td>
                  <td>{trx.nasabah}</td>
                  <td>{trx.no_rekening}</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'}>
                    Rp {trx.jumlah.toLocaleString()}
                  </td>
                  <td>{trx.keterangan}</td>
                  <td>{trx.petugas}</td>
                </tr>
              ))
            ) : (
              <tr>
                <td colSpan="7" className="text-center">Tidak ada data transaksi</td>
              </tr>
            )}
          </tbody>
        </table>
      </div>
    )}
  </div>
);

Jangan lupa export komponen.

💡 Catatan: Endpoint /api/accounts untuk dropdown perlu dibuat jika belum ada. Bisa ditambahkan di AdminController dengan method accounts() yang mengembalikan daftar akun (id, account_number, user.name).

Langkah 5: Menambahkan Route dengan Proteksi Admin

Di src/App.js, tambahkan route baru:

import AdminReports from './pages/AdminReports';

// ... di dalam Routes
<Route path="/admin/reports" element={
  <PrivateRoute allowedRoles={['admin']}>
    <AdminReports />
  </PrivateRoute>
} />

Jangan lupa update PrivateRoute untuk mendukung allowedRoles (sudah dibuat di tutorial #13).

Langkah 6: Menambahkan Link di Navbar Admin

Di Navbar.js, untuk role admin tambahkan menu "Laporan Transaksi":

{user.role === 'admin' && (
  <li className="nav-item">
    <Link className="nav-link" to="/admin/reports">Laporan</Link>
  </li>
)}
😆 "Dengan fitur laporan, admin bisa tahu siapa nasabah paling rajin nabung dan siapa yang suka jajan!" 🍩

Langkah 7: Uji Coba

  1. Login sebagai admin.
  2. Masuk ke halaman Laporan Transaksi (misal /admin/reports).
  3. Tanpa filter, seharusnya muncul semua transaksi.
  4. Coba filter berdasarkan tanggal (misal 1-31 Maret 2025) dan klik Cari. Data berubah.
  5. Pilih akun tertentu, filter.
  6. Klik Export Excel → file .xlsx terdownload, buka dengan Excel.
  7. Klik Export PDF → file .pdf terdownload, berisi tabel.
  8. Klik Reset → filter kembali kosong, data kembali semua.

Kesimpulan

  • ✅ Endpoint API dengan filter berhasil dibuat.
  • ✅ Halaman laporan di React dengan filter tanggal dan akun.
  • ✅ Export ke Excel dan PDF berfungsi dengan baik.
  • ✅ Hanya admin yang bisa mengakses.

Di tutorial terakhir (#20: Finalisasi dan Penyempurnaan Aplikasi) kita akan melakukan pengecekan akhir, memperbaiki bug, dan menambahkan sentuhan akhir agar aplikasi siap digunakan. Sampai jumpa!


Daftar Tutorial

  • Tutorial #20: Finalisasi dan Penyempurnaan Aplikasi
Lebih baru Lebih lama

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