Sesi 9: Finalizing Features (Layout, List & Payments)

Sesi 9: Finalizing Features

Persistent Layout, Customer List, & Payment Operations

1. Persistent Layout (Sidebar Navigation)

Untuk tampilan aplikasi profesional, kita memerlukan Layout yang memuat Sidebar di kiri dan Konten di kanan. Ini memudahkan navigasi antar menu.

File: frontend/src/components/Layout.tsx

import React, { useState } from 'react'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { LayoutDashboard, Users, CreditCard, LogOut, Menu, X, DollarSign } from 'lucide-react'; export default function Layout() { const [sidebarOpen, setSidebarOpen] = useState(false); const location = useLocation(); const navigate = useNavigate(); const handleLogout = () => { localStorage.removeItem('access_token'); navigate('/'); }; const navItems = [ { path: '/dashboard', label: 'Dashboard', icon: LayoutDashboard }, { path: '/customers', label: 'Data Nasabah', icon: Users }, { path: '/loans/new', label: 'Pengajuan Baru', icon: CreditCard }, { path: '/payments', label: 'Pembayaran', icon: DollarSign }, ]; return (
{/* Mobile Sidebar Backdrop */} {sidebarOpen && (
setSidebarOpen(false)} /> )} {/* Sidebar */} {/* Main Content */}
{/* Mobile Header */}
BPRS AO
{/* Main Scrollable Area */}
); }

2. Halaman Data Nasabah (List)

Menampilkan tabel nasabah dengan fitur pencarian. Sangat penting agar AO bisa mencari ID Nasabah sebelum membuat pembiayaan.

File: frontend/src/pages/Customers.tsx

import React, { useEffect, useState } from 'react'; import apiClient from '../api/client'; import { Search, Plus, User } from 'lucide-react'; interface Customer { id: number; name: string; phone: string; national_id: string | null; business_type: string | null; } export default function Customers() { const [customers, setCustomers] = useState([]); const [search, setSearch] = useState(''); useEffect(() => { fetchCustomers(); }, []); const fetchCustomers = async () => { try { const res = await apiClient.get('/customers/'); setCustomers(res.data); } catch (error) { console.error(error); } }; const filtered = customers.filter(c => c.name.toLowerCase().includes(search.toLowerCase()) || (c.national_id && c.national_id.includes(search)) ); return (

Data Nasabah

{/* Search Bar */}
setSearch(e.target.value)} />
{/* Table */}
{filtered.map((c) => ( ))} {filtered.length === 0 && ( )}
ID Nama Lengkap No. KTP Usaha No. HP
#{c.id}
{c.name}
{c.national_id || '-'} {c.business_type || '-'} {c.phone}
Data tidak ditemukan.
); }

3. Halaman Pembayaran Angsuran (Operational)

Fitur utama AO. Mencari berdasarkan ID Pembiayaan, melihat jadwal, dan melakukan pembayaran.

File: frontend/src/pages/Payments.tsx

import React, { useState } from 'react'; import apiClient from '../api/client'; import { Search, CheckCircle, Clock, AlertTriangle } from 'lucide-react'; export default function Payments() { const [searchId, setSearchId] = useState(''); const [loading, setLoading] = useState(false); const [schedule, setSchedule] = useState([]); const [loanInfo, setLoanInfo] = useState(null); const [error, setError] = useState(''); const handleSearch = async () => { if (!searchId) return; setLoading(true); setError(''); try { // Asumsi API Backend menerima loan_id di URL const res = await apiClient.get(`/payments/${searchId}`); setSchedule(res.data); // Kita asumsikan response header punya info loan, atau kita fetch terpisah // Di sini kita fetch detail loan untuk menampilkan nama nasabah const loanRes = await apiClient.get(`/loans/${searchId}`); setLoanInfo(loanRes.data); } catch (err: any) { setError('Data pembiayaan tidak ditemukan.'); setSchedule([]); setLoanInfo(null); } finally { setLoading(false); } }; const handlePay = async (paymentId: number) => { if (!confirm('Konfirmasi pembayaran angsuran ini?')) return; try { // Panggil API Bayar (Endpoint Sesi 6) // Asumsi backend menerima amount otomatis Full Payment const amount = schedule.find((p: any) => p.id === paymentId)?.amount_due || 0; await apiClient.post(`/payments/${paymentId}/pay`, { amount: amount }); // Refresh data handleSearch(); alert('Pembayaran Berhasil!'); } catch (err) { alert('Gagal melakukan pembayaran.'); } }; const getStatusBadge = (status: string) => { switch (status) { case 'on_time': return LUNAS; case 'late': return TERLAMBAT; default: return JATUH TEMPO; } }; return (

Pembayaran Angsuran

{/* Search Section */}
setSearchId(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleSearch()} />
{error &&

{error}

}
{/* Loan Info & Schedule */} {loanInfo && ( <>

Nasabah

{loanInfo.customer?.name}

Plafond

Rp {new Intl.NumberFormat('id-ID').format(loanInfo.principal_amount)}

{schedule.map((item) => ( ))}
Jatuh Tempo Jatuh Tempo Status Aksi
{new Date(item.schedule_date).toLocaleDateString('id-ID')} Rp {new Intl.NumberFormat('id-ID').format(item.amount_due)} {getStatusBadge(item.status)} {item.amount_paid >= item.amount_due ? ( Sudah Dibayar ) : ( )}
)}
); }

4. Finalisasi Routing (App.tsx)

Kita mengupdate App.tsx untuk membungkus semua halaman yang dilindungi (Protected Routes) di dalam Layout Component.

File: frontend/src/App.tsx

import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import Layout from './components/Layout'; // Import Layout Baru import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import Customers from './pages/Customers'; // Import List Nasabah import CreateCustomer from './pages/CreateCustomer'; import CreateLoan from './pages/CreateLoan'; import Payments from './pages/Payments'; // Import Halaman Bayar const ProtectedWrapper = () => ( ); // Helper untuk Protected Route (jika ingin tetap mempertahankan logika Auth sebelumnya) const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const token = localStorage.getItem('access_token'); if (!token) return ; return <>{children}; }; function App() { return ( } /> {/* Semua route di bawah ini dibungkus oleh Layout Sidebar */} }> } /> } /> } /> } /> } /> ); } export default App;

5. Final Step: Build Production

Semua fitur sudah lengkap. Sekarang kita compile kode React agar ringan dan siap diakses publik via Nginx.

cd /www/wwwroot/ao.baktimakmur.com/frontend npm run build

Perintah di atas akan membuat folder frontend/build berisi file index.html dan static/ yang sudah dioptimasi.