# Frontend Development Plan
**BTM Microbanking System**

**Last Updated:** February 3, 2026

---

## Recent Progress (February 3, 2026)

### Completed Work

#### ✅ Members Module (Complete)
- **Create.tsx** - Full member registration form (374 lines)
- **Edit.tsx** - Member profile editing form (351 lines)
- **Show.tsx** - Member details view with stats
- **Index.tsx** - Members listing with search, filters, and pagination
- **Type Definitions** - `member.ts` aligned with database schema
- **Fixed Issues:**
  - Select components now use controlled `useForm` pattern
  - Gender values changed from `L/P` to `male/female` to match database
  - Type definitions rewritten to match migration exactly
  - Form request validation updated (removed non-existent fields)
  - Date formatting helper added for HTML5 date input

#### ✅ Savings Module (Complete)
- **Index.tsx** - Complete listing page (438 lines)
  - DataTable with TanStack Table (sorting, pagination)
  - Search by account number, member name
  - Filters: Branch, Product, Status
  - Stats cards: Total accounts, Total balance, Active accounts
  - CSV Export functionality
- **Show.tsx** - Account details page (~350 lines)
  - Account info header with status badge
  - Stats cards: Balance, Hold, Available Balance
  - Quick actions: Deposit, Withdrawal, Close (embedded modals)
  - Member info card with link
  - Recent transactions (last 10)
- **OpenAccount.tsx** - Open new account form (216 lines)
  - Pre-fills member from URL param
  - Product selection with requirements display
  - Form validation
- **Transactions.tsx** - Transaction history
  - Full transaction listing with filters
  - Export to CSV
- **Type Definitions** - `savings.ts` with complete interfaces
- **Backend Fixes:**
  - SavingsController relationship loading fixed
  - Missing filters added (branch_id, product_id)

#### ✅ Service Layer Fix
- **MemberService.php** - Fixed constructor for SavingsService injection
  - Changed from nullable `?SavingsService = null` to required `SavingsService`
  - Now automatically creates default savings account on member registration
  - Uses first active product by ID (Simpanan Wajib - main product)

### Technical Patterns Used

#### Controlled Components Pattern
Since shadcn Select components are controlled React components, we use the `useForm` hook instead of Inertia's `<Form>` component:

```typescript
import { useForm } from '@inertiajs/react'

const form = useForm({
  gender: '',
  branch_id: '',
  name: '',
  // ...
})

const handleSubmit = (e: { preventDefault: () => void }) => {
  e.preventDefault()
  form.post(route('members.store'))
}

// In JSX:
<Select
  value={form.data.gender}
  onValueChange={(value) => form.setData('gender', value)}
>
  <SelectTrigger>
    <SelectValue placeholder="Pilih jenis kelamin" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="male">Laki-laki</SelectItem>
    <SelectItem value="female">Perempuan</SelectItem>
  </SelectContent>
</Select>
```

#### Date Formatting Helper
HTML5 date input requires `YYYY-MM-DD` format:

```typescript
const formatForDateInput = (dateStr: string | null | undefined): string => {
  if (!dateStr) return ''
  const date = new Date(dateStr)
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}

// Usage:
<form.data.birth_date = formatForDateInput(member.birth_date)
```

#### Currency Formatting
Indonesian Rupiah formatter:

```typescript
export function formatIDR(amount: number): string {
  return new Intl.NumberFormat('id-ID', {
    style: 'currency',
    currency: 'IDR',
    minimumFractionDigits: 0,
    maximumFractionDigits: 0,
  }).format(amount)
}
```

### Lessons Learned

1. **shadcn Select Components** - Cannot use native `name` attribute, must use controlled pattern with `value` and `onValueChange`
2. **Database Schema Alignment** - Type definitions must exactly match database migrations
3. **Gender Values** - Use database-friendly values (`male`/`female`) instead of localized abbreviations (`L`/`P`)
4. **Dependency Injection** - Remove nullable type hints to force Laravel to inject services
5. **Form Request Validation** - Remove validation rules for non-existent database columns

### Next Steps

- ⏳ Financing module - Complete remaining pages (Installments, Payments)
- ⏳ Financing module - Testing and bug fixes
- ⏳ Branches module frontend upgrade
- ⏳ Receipt PDF generation
- ⏳ Transaction export (Excel/PDF)
- ⏳ Manual testing of all forms

---

## Financing Module Progress (February 5, 2026)

### ✅ Completed (Core Workflow)

#### 1. Infrastructure
- **TypeScript Types** - `resources/js/types/financing.ts` (complete)
  - Financing, FinancingProduct, FinancingInstallment, FinancingPayment
  - Filters, form data types, calculation types
  - All aligned with Laravel backend models

- **StatusBadge Configuration** - Updated with all statuses
  - Financing: proposed, approved, disbursed, active, paid_off, rejected, written_off
  - Collectibility: 1-5 (Lancar to Macet)
  - Installment: pending, partial, paid, skipped, overdue

- **Wayfinder Routes** - Generated all TypeScript route functions
  - Index, Show, Create, Store, Approve, Reject, Disburse
  - Installments, Payments, RecordPayment

#### 2. Index Page ✅
**File:** `resources/js/pages/Financings/Index.tsx` (~554 lines)

**Features:**
- Stats cards: Total Applications, Total Balance, Active Financings, Overdue
- Filters: Search, Branch, Product, Status
- DataTable with TanStack Table
- Actions dropdown (View, Approve, Reject, Disburse, Record Payment, Schedule, Payments)
- CSV export
- Pagination
- Authorization checks (Manager vs Teller)

#### 3. Show Page ✅
**File:** `resources/js/pages/Financings/Show.tsx` (~450 lines)

**Features:**
- Header with account number, status badges, collectibility
- Member info card (link to member)
- Product info card
- Stats cards: Principal, Margin, Total Repayment, Daily Installment
- Balance display with progress bar
- Days remaining/overdue calculation
- Action buttons based on status
- Disbursement modal (Savings/Cash selection)
- Installment summary (Paid, Pending, Overdue counts)
- Recent payments (last 5)

#### 4. Create Page ✅
**File:** `resources/js/pages/Financings/Create.tsx` (~400 lines)

**Features:**
- Member info card (read-only, pre-filled)
- Product selection with real-time calculation
- Amount input with min/max validation
- Purpose textarea
- Collector assignment (optional)
- Real-time calculation panel:
  - Principal, Margin, Total Repayment
  - Daily Installment
  - Fee breakdown (Admin, Provisi, Notaris, Survey)
  - Net Disbursement
- Duplicate prevention check
- Collectibility warning

#### 5. Payment Recording Page ✅
**File:** `resources/js/pages/Financings/RecordPayment.tsx` (~550 lines)

**Features:**
- Financing info card (balance, next due, collectibility)
- Payment form: Amount, Method, Date, Notes
- Quick amount buttons (Next installment, Total overdue, Daily)
- Allocation mode toggle (Auto/Manual)
- Auto-allocation preview (FIFO - oldest first)
- Manual allocation with installment selection
- Real-time calculation
- Validation for manual mode (must match selected total)

#### 6. Approval Modal ✅
**File:** `resources/js/pages/Financings/Components/ApprovalModal.tsx` (~200 lines)

**Features:**
- Financing summary display
- Approved amount input (with min/max validation)
- Real-time recalculation
- Notes field
- Collectibility warning (4-5)
- Confirm/Cancel buttons

### ⏳ Remaining Implementation

#### 7. Installments Page (Template Ready)
**File:** `resources/js/pages/Financings/Installments.tsx` (to be created)

**Planned Features:**
- Header with financing info
- Schedule summary (Paid, Pending, Overdue)
- Filters: Status, Date range
- Schedule table (100 installments)
- Sunday handling (special styling)
- Status badges
- Print button with print styles
- Export to CSV

**See template below**

#### 8. Payments Page (Template Ready)
**File:** `resources/js/pages/Financings/Payments.tsx` (to be created)

**Planned Features:**
- Filters: Date range, Payment method
- Payment table with installment breakdown
- CSV export
- PDF receipt download (backend route ready)

**See template below**

### Technical Patterns Used

#### Real-time Calculation Hook
```typescript
const calculation = useMemo(() => {
    const productId = Number(form.data.financing_product_id);
    const amount = Number(form.data.requested_amount);

    if (!productId || !amount || amount <= 0) return null;

    const product = products.find((p) => p.id === productId);
    if (!product) return null;

    const principal = amount;
    const margin = principal * (product.margin_rate / 100);
    const total = principal + margin;
    const daily = total / product.installment_count;

    return { principal, margin, total, daily };
}, [form.data.financing_product_id, form.data.requested_amount, products]);
```

#### Auto-Allocation (FIFO)
```typescript
const autoAllocation = useMemo(() => {
    const amount = Number(paymentAmount);
    const installments = pendingInstallments.sort(
        (a, b) => new Date(a.due_date).getTime() - new Date(b.due_date).getTime()
    );

    let remaining = amount;
    const allocation = installments.map((installment) => {
        if (remaining <= 0) return null;

        const outstanding = installment.total_amount - installment.total_paid;
        const allocated = Math.min(remaining, outstanding);
        remaining -= allocated;

        return { installment, amount: allocated };
    }).filter(Boolean);

    return { allocation, remaining };
}, [paymentAmount, pendingInstallments]);
```

#### Status-Based Actions
```typescript
{financing.status === 'proposed' && isManager && (
    <>
        <Button onClick={handleApprove}>Setujui</Button>
        <Button onClick={handleReject}>Tolak</Button>
    </>
)}

{financing.status === 'approved' && isManager && (
    <Button onClick={handleDisburse}>Cairkan Dana</Button>
)}

{financing.status === 'active' && isTeller && (
    <Link href={recordPaymentRoute}>Catat Pembayaran</Link>
)}
```

### Integration Points

#### Backend Controllers
- `FinancingController@index` - Index with filters/pagination
- `FinancingController@show` - Details with installments/payments
- `FinancingController@create` - Create form with member data
- `FinancingController@store` - Store application
- `FinancingController@approve` - Approve with amount
- `FinancingController@reject` - Reject with notes
- `FinancingController@disburse` - Disburse with method
- `FinancingController@installments` - Installment schedule
- `FinancingController@payments` - Payment history
- `PaymentController@store` - Record payment

#### Backend Services
- `FinancingService@createApplication` - Validation, duplicate check
- `FinancingService@approveApplication` - Update status, generate schedule
- `FinancingService@disburse` - Journal entries, balance update
- `FinancingService@recordPayment` - Allocation, balance update, collectibility

### Testing Checklist
- [ ] Create financing from list page
- [ ] Create financing with real-time calculations
- [ ] Approve financing with modal (adjust amount)
- [ ] Reject financing with reason
- [ ] Disburse with cash/savings selection
- [ ] Record payment with auto-allocation
- [ ] Record payment with manual allocation
- [ ] View installment schedule
- [ ] View payment history
- [ ] Export to CSV from all pages
- [ ] Check collectibility updates after payments
- [ ] Verify authorization (manager vs teller)

---

## Remaining Page Templates

### Installments Page Template

**File:** `resources/js/pages/Financings/Installments.tsx` (~350 lines)

```typescript
import { Button } from '@/components/ui/button';
import { DataTable } from '@/components/ui/data-table';
import { StatusBadge } from '@/components/ui/status-badge';
import AppLayout from '@/layouts/app-layout';
import { exportToCSV } from '@/lib/exportToCSV';
import { formatDateID, formatIDR } from '@/lib/formatters';
import { useFilterParams } from '@/lib/hooks/useFilterParams';
import financingsRoutes from '@/routes/financings';
import type { BreadcrumbItem } from '@/types';
import type { Financing, FinancingInstallment } from '@/types/financing';
import { Head, Link } from '@inertiajs/react';
import { ColumnDef } from '@tanstack/react-table';
import { ArrowLeft, Calendar, FileText, Printer } from 'lucide-react';
import { useMemo, useState } from 'react';

interface PageProps {
    financing: Financing & {
        installments: FinancingInstallment[];
    };
}

export default function Installments({ financing }: PageProps) {
    const [statusFilter, setStatusFilter] = useState<string>('all');
    const [dateFromFilter, setDateFromFilter] = useState<string>('');
    const [dateToFilter, setDateToFilter] = useState<string>('');

    // Calculate summary stats
    const summary = useMemo(() => {
        const paid = financing.installments.filter(i => i.status === 'paid').length;
        const pending = financing.installments.filter(i => i.status === 'pending').length;
        const partial = financing.installments.filter(i => i.status === 'partial').length;
        const overdue = financing.installments.filter(
            i => new Date(i.due_date) < new Date() && i.status !== 'paid'
        ).length;

        return { paid, pending, partial, overdue };
    }, [financing.installments]);

    // Filter installments
    const filteredInstallments = useMemo(() => {
        return financing.installments.filter(installment => {
            // Status filter
            if (statusFilter !== 'all') {
                if (statusFilter === 'overdue') {
                    const isOverdue = new Date(installment.due_date) < new Date();
                    if (!isOverdue || installment.status === 'paid') return false;
                } else if (installment.status !== statusFilter) {
                    return false;
                }
            }

            // Date range filter
            if (dateFromFilter && new Date(installment.due_date) < new Date(dateFromFilter)) {
                return false;
            }
            if (dateToFilter && new Date(installment.due_date) > new Date(dateToFilter)) {
                return false;
            }

            return true;
        });
    }, [financing.installments, statusFilter, dateFromFilter, dateToFilter]);

    // Check if date is Sunday
    const isSunday = (dateStr: string) => {
        const date = new Date(dateStr);
        return date.getDay() === 0;
    };

    // Get installment status
    const getInstallmentStatus = (installment: FinancingInstallment) => {
        if (installment.status === 'paid') return 'paid';
        if (installment.status === 'skipped') return 'skipped';

        const isOverdue = new Date(installment.due_date) < new Date() && installment.status !== 'paid';
        if (installment.status === 'partial') return isOverdue ? 'partial-overdue' : 'partial';
        if (isOverdue) return 'overdue';

        return 'pending';
    };

    const columns: ColumnDef<FinancingInstallment>[] = [
        {
            accessorKey: 'installment_number',
            header: 'No',
            cell: ({ row }) => row.getValue('installment_number'),
        },
        {
            accessorKey: 'due_date',
            header: 'Jatuh Tempo',
            cell: ({ row }) => {
                const date = row.getValue('due_date') as string;
                const sunday = isSunday(date);

                return (
                    <span className={sunday ? 'text-purple-600 font-medium' : ''}>
                        {formatDateID(date)}
                        {sunday && <span className="ml-2 text-xs">(Minggu)</span>}
                    </span>
                );
            },
        },
        {
            accessorKey: 'principal_amount',
            header: 'Pokok',
            cell: ({ row }) => formatIDR(row.getValue('principal_amount')),
        },
        {
            accessorKey: 'margin_amount',
            header: 'Margin',
            cell: ({ row }) => formatIDR(row.getValue('margin_amount')),
        },
        {
            accessorKey: 'total_amount',
            header: 'Total',
            cell: ({ row }) => formatIDR(row.getValue('total_amount')),
        },
        {
            accessorKey: 'total_paid',
            header: 'Dibayar',
            cell: ({ row }) => formatIDR(row.getValue('total_paid')),
        },
        {
            accessorKey: 'status',
            header: 'Status',
            cell: ({ row }) => {
                const installment = row.original;
                const status = getInstallmentStatus(installment);
                return <StatusBadge type="installment" status={status} />;
            },
        },
        {
            accessorKey: 'paid_date',
            header: 'Tgl Bayar',
            cell: ({ row }) => formatDateID(row.getValue('paid_date')),
        },
    ];

    const handlePrint = () => {
        window.print();
    };

    const handleExportCSV = () => {
        exportToCSV(
            ['No', 'Jatuh Tempo', 'Pokok', 'Margin', 'Total', 'Dibayar', 'Status', 'Tgl Bayar'],
            filteredInstallments.map(i => ({
                No: i.installment_number,
                'Jatuh Tempo': i.due_date,
                Pokok: i.principal_amount.toString(),
                Margin: i.margin_amount.toString(),
                Total: i.total_amount.toString(),
                Dibayar: i.total_paid.toString(),
                Status: i.status,
                'Tgl Bayar': i.paid_date || '-',
            })),
            `jadwal-${financing.account_number}.csv`
        );
    };

    return (
        <AppLayout>
            <Head title={`Jadwal Angsuran - ${financing.account_number}`} />

            <div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
                {/* Header */}
                <div className="flex items-center gap-4 no-print">
                    <Button variant="outline" size="icon" asChild>
                        <Link href={financingsRoutes.show({ financing: financing.id }).url}>
                            <ArrowLeft className="h-4 w-4" />
                        </Link>
                    </Button>
                    <div className="flex flex-1 flex-col gap-1">
                        <h1 className="text-2xl font-black">Jadwal Angsuran</h1>
                        <span className="text-sm text-muted-foreground">
                            {financing.account_number} • {financing.member.name}
                        </span>
                    </div>
                    <div className="flex gap-2">
                        <Button variant="outline" onClick={handleExportCSV}>
                            Export CSV
                        </Button>
                        <Button variant="outline" onClick={handlePrint}>
                            <Printer className="mr-2 h-4 w-4" />
                            Cetak
                        </Button>
                    </div>
                </div>

                {/* Summary Stats */}
                <div className="grid gap-4 md:grid-cols-4">
                    <Card>
                        <div className="text-2xl font-bold">{financing.installments.length}</div>
                        <div className="text-sm text-muted-foreground">Total Angsuran</div>
                    </Card>
                    <Card>
                        <div className="text-2xl font-bold text-green-600">{summary.paid}</div>
                        <div className="text-sm text-muted-foreground">Sudah Bayar</div>
                    </Card>
                    <Card>
                        <div className="text-2xl font-bold text-yellow-600">{summary.pending + summary.partial}</div>
                        <div className="text-sm text-muted-foreground">Tertunggak</div>
                    </Card>
                    <Card>
                        <div className="text-2xl font-bold text-destructive">{summary.overdue}</div>
                        <div className="text-sm text-muted-foreground">Telat Bayar</div>
                    </Card>
                </div>

                {/* Filters */}
                <div className="flex flex-wrap gap-4 no-print">
                    <Select value={statusFilter} onValueChange={setStatusFilter}>
                        <SelectTrigger className="w-48">
                            <SelectValue placeholder="Semua Status" />
                        </SelectTrigger>
                        <SelectContent>
                            <SelectItem value="all">Semua Status</SelectItem>
                            <SelectItem value="pending">Belum Bayar</SelectItem>
                            <SelectItem value="partial">Sebagian</SelectItem>
                            <SelectItem value="paid">Lunas</SelectItem>
                            <SelectItem value="overdue">Telat Bayar</SelectItem>
                        </SelectContent>
                    </Select>

                    <Input
                        type="date"
                        value={dateFromFilter}
                        onChange={(e) => setDateFromFilter(e.target.value)}
                        className="w-48"
                    />
                    <Input
                        type="date"
                        value={dateToFilter}
                        onChange={(e) => setDateToFilter(e.target.value)}
                        className="w-48"
                    />
                </div>

                {/* Schedule Table */}
                <DataTable
                    columns={columns}
                    data={filteredInstallments}
                    pagination={{
                        currentPage: 1,
                        lastPage: 1,
                        onNext: () => {},
                        onPrev: () => {},
                    }}
                />
            </div>

            <style>{`
                @media print {
                    .no-print { display: none !important; }
                    .schedule-table { page-break-inside: avoid; }
                }
            `}</style>
        </AppLayout>
    );
}
```

### Payments Page Template

**File:** `resources/js/pages/Financings/Payments.tsx` (~300 lines)

```typescript
import { Button } from '@/components/ui/button';
import { DataTable } from '@/components/ui/data-table';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import AppLayout from '@/layouts/app-layout';
import { exportToCSV } from '@/lib/exportToCSV';
import { formatDateID, formatIDR } from '@/lib/formatters';
import financingsRoutes from '@/routes/financings';
import type { BreadcrumbItem } from '@/types';
import type { Financing, FinancingPayment, PaymentMethod } from '@/types/financing';
import { Head, Link } from '@inertiajs/react';
import { ColumnDef } from '@tanstack/react-table';
import { ArrowLeft, Download, FileText } from 'lucide-react';
import { useMemo, useState } from 'react';

interface PageProps {
    financing: Financing & {
        payments: FinancingPayment[];
    };
}

export default function Payments({ financing }: PageProps) {
    const [dateFrom, setDateFrom] = useState<string>('');
    const [dateTo, setDateTo] = useState<string>('');
    const [methodFilter, setMethodFilter] = useState<PaymentMethod | 'all'>('all');

    // Filter payments
    const filteredPayments = useMemo(() => {
        return financing.payments.filter(payment => {
            if (methodFilter !== 'all' && payment.payment_method !== methodFilter) {
                return false;
            }
            if (dateFrom && new Date(payment.payment_date) < new Date(dateFrom)) {
                return false;
            }
            if (dateTo && new Date(payment.payment_date) > new Date(dateTo)) {
                return false;
            }
            return true;
        });
    }, [financing.payments, methodFilter, dateFrom, dateTo]);

    const columns: ColumnDef<FinancingPayment>[] = [
        {
            accessorKey: 'payment_number',
            header: 'Nomor Pembayaran',
            cell: ({ row }) => (
                <span className="font-mono text-sm">{row.getValue('payment_number')}</span>
            ),
        },
        {
            accessorKey: 'payment_date',
            header: 'Tanggal',
            cell: ({ row }) => formatDateID(row.getValue('payment_date')),
        },
        {
            accessorKey: 'amount',
            header: 'Jumlah',
            cell: ({ row }) => (
                <span className="font-bold">{formatIDR(row.getValue('amount'))}</span>
            ),
        },
        {
            accessorKey: 'principal_portion',
            header: 'Pokok',
            cell: ({ row }) => formatIDR(row.getValue('principal_portion')),
        },
        {
            accessorKey: 'margin_portion',
            header: 'Margin',
            cell: ({ row }) => formatIDR(row.getValue('margin_portion')),
        },
        {
            accessorKey: 'payment_method',
            header: 'Metode',
            cell: ({ row }) => {
                const method = row.getValue('payment_method') as PaymentMethod;
                const labels = {
                    cash: 'Tunai',
                    transfer: 'Transfer',
                    auto_debit: 'Auto Debit',
                };
                return labels[method];
            },
        },
        {
            id: 'actions',
            header: 'Aksi',
            cell: ({ row }) => {
                const payment = row.original;
                return (
                    <Button
                        variant="outline"
                        size="sm"
                        onClick={() => handleDownloadReceipt(payment)}
                    >
                        <Download className="mr-2 h-4 w-4" />
                        Bukti
                    </Button>
                );
            },
        },
    ];

    const handleDownloadReceipt = (payment: FinancingPayment) => {
        // Backend route for PDF receipt
        window.open(`/payments/${payment.id}/receipt`, '_blank');
    };

    const handleExportCSV = () => {
        exportToCSV(
            ['Nomor Pembayaran', 'Tanggal', 'Jumlah', 'Pokok', 'Margin', 'Metode', 'Catatan'],
            filteredPayments.map(p => ({
                'Nomor Pembayaran': p.payment_number,
                Tanggal: p.payment_date,
                Jumlah: p.amount.toString(),
                Pokok: p.principal_portion.toString(),
                Margin: p.margin_portion.toString(),
                Metode: p.payment_method,
                Catatan: p.notes || '-',
            })),
            `pembayaran-${financing.account_number}.csv`
        );
    };

    return (
        <AppLayout>
            <Head title={`Riwayat Pembayaran - ${financing.account_number}`} />

            <div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
                {/* Header */}
                <div className="flex items-center gap-4">
                    <Button variant="outline" size="icon" asChild>
                        <Link href={financingsRoutes.show({ financing: financing.id }).url}>
                            <ArrowLeft className="h-4 w-4" />
                        </Link>
                    </Button>
                    <div className="flex flex-1 flex-col gap-1">
                        <h1 className="text-2xl font-black">Riwayat Pembayaran</h1>
                        <span className="text-sm text-muted-foreground">
                            {financing.account_number} • {financing.member.name}
                        </span>
                    </div>
                    <Button variant="outline" onClick={handleExportCSV}>
                        <FileText className="mr-2 h-4 w-4" />
                        Export CSV
                    </Button>
                </div>

                {/* Summary */}
                <div className="grid gap-4 md:grid-cols-3">
                    <Card>
                        <div className="text-2xl font-bold">{filteredPayments.length}</div>
                        <div className="text-sm text-muted-foreground">Total Transaksi</div>
                    </Card>
                    <Card>
                        <div className="text-2xl font-bold">
                            {formatIDR(filteredPayments.reduce((sum, p) => sum + p.amount, 0))}
                        </div>
                        <div className="text-sm text-muted-foreground">Total Dibayar</div>
                    </Card>
                    <Card>
                        <div className="text-2xl font-bold">
                            {formatIDR(filteredPayments.reduce((sum, p) => sum + p.principal_portion, 0))}
                        </div>
                        <div className="text-sm text-muted-foreground">Total Pokok Dibayar</div>
                    </Card>
                </div>

                {/* Filters */}
                <div className="flex flex-wrap gap-4">
                    <Input
                        type="date"
                        value={dateFrom}
                        onChange={(e) => setDateFrom(e.target.value)}
                        className="w-48"
                    />
                    <Input
                        type="date"
                        value={dateTo}
                        onChange={(e) => setDateTo(e.target.value)}
                        className="w-48"
                    />
                    <Select value={methodFilter} onValueChange={(value: any) => setMethodFilter(value)}>
                        <SelectTrigger className="w-48">
                            <SelectValue placeholder="Semua Metode" />
                        </SelectTrigger>
                        <SelectContent>
                            <SelectItem value="all">Semua Metode</SelectItem>
                            <SelectItem value="cash">Tunai</SelectItem>
                            <SelectItem value="transfer">Transfer</SelectItem>
                            <SelectItem value="auto_debit">Auto Debit</SelectItem>
                        </SelectContent>
                    </Select>
                </div>

                {/* Payments Table */}
                <DataTable
                    columns={columns}
                    data={filteredPayments}
                    pagination={{
                        currentPage: 1,
                        lastPage: 1,
                        onNext: () => {},
                        onPrev: () => {},
                    }}
                />
            </div>
        </AppLayout>
    );
}
```

### Backend Implementation Needed

#### PDF Receipt Generation
**File:** `app/Http/Controllers/PaymentController.php`

```php
public function receipt(FinancingPayment $payment)
{
    $payment->load(['financing.member', 'financing.financingProduct']);
    $installments = $this->financingService->getPaymentAllocation($payment);

    $pdf = PDF::loadView('pdfs.payment-receipt', [
        'payment' => $payment,
        'installments' => $installments,
    ]);

    return $pdf->download("receipt-{$payment->payment_number}.pdf");
}
```

**Route:** `GET /payments/{payment}/receipt`

### Quick Start Guide

To complete the remaining pages:

1. **Installments Page** (30 minutes):
   - Copy template above
   - Import Card component
   - Adjust styling to match project conventions
   - Test print functionality
   - Test Sunday highlighting

2. **Payments Page** (20 minutes):
   - Copy template above
   - Import Card component
   - Test CSV export
   - Verify PDF receipt route exists in backend

3. **Testing** (1-2 hours):
   - Run full workflow test
   - Test all filters
   - Test exports
   - Test print
   - Fix any bugs

**Estimated Total Time:** 2-3 hours to complete remaining pages

---

## Project Overview

This document outlines the frontend development plan for the BTM Microbanking System, a Laravel 12 + React 19 + Inertia v2 application with Tailwind CSS v4.

**Stack:**
- **Backend:** Laravel 12 (PHP 8.3+)
- **Frontend:** React 19 + Inertia v2
- **Styling:** Tailwind CSS v4
- **Routing:** Laravel Wayfinder (TypeScript routes)
- **UI:** shadcn/ui components (Radix UI primitives)
- **Tables:** TanStack Table (via shadcn DataTable)
- **Charts:** Recharts
- **Forms:** Inertia v2 Form components
- **Testing:** Vitest (unit) + Playwright (E2E)

---

## Current State

### ✅ Completed
- Build setup (Vite + React compiler)
- UI component library (Radix + Tailwind)
- Layout system (app-layout, auth-layout)
- Navigation with sidebar (app-sidebar.tsx)
- Dashboard (fully functional with stats)
- Authentication pages (Login, Register, Password Reset, 2FA)
- Settings pages (Profile, Password, Appearance, Two-Factor)

### 🚧 Partial
- Members Index (UI exists, needs backend integration)
- Savings Index (UI exists, needs backend integration)
- Financing Index (UI exists, needs backend integration)
- Branches Index (basic implementation, needs upgrade)

### ❌ Missing
- Data fetching and API integration
- Form validation and submission
- CRUD operations for all entities
- Detail pages (Show, Edit, Create)
- Table components with TanStack Table
- Loading states and error handling
- Search and filtering
- Export to CSV functionality
- Dark mode support

---

## Backend Routes Available

### Members
- `GET /members` - Index with search/filter/pagination
- `GET /members/create` - Create form
- `POST /members` - Store new member
- `GET /members/{member}` - Show member details
- `GET /members/{member}/edit` - Edit form
- `PUT /members/{member}` - Update member
- `DELETE /members/{member}` - Delete member
- `POST /members/{member}/verify` - Verify member

### Savings
- `GET /savings` - Index list
- `GET /savings/open/{member}` - Open account form
- `POST /savings/open` - Open new account
- `GET /savings/{account}` - Show account details
- `POST /savings/deposit` - Deposit transaction
- `POST /savings/withdraw` - Withdraw transaction
- `POST /savings/{account}/close` - Close account

### Financing
- `GET /financing` - Index list
- `GET /financing/create/{member}` - Create application
- `POST /financing` - Store application
- `GET /financing/{financing}` - Show details
- `POST /financing/{financing}/approve` - Approve
- `POST /financing/{financing}/reject` - Reject
- `POST /financing/{financing}/disburse` - Disburse funds
- `POST /financing/payments` - Record payment
- `GET /financing/{financing}/installments` - Payment schedule
- `GET /financing/{financing}/payments` - Payment history

### Branches
- `GET /branches` - Index list
- `GET /branches/create` - Create form
- `POST /branches` - Store new branch
- `GET /branches/{branch}` - Show details
- `GET /branches/{branch}/edit` - Edit form
- `PUT /branches/{branch}` - Update branch
- `DELETE /branches/{branch}` - Delete branch

---

## Implementation Strategy

### Phase 1: Foundation Components (Week 1)

#### 1.1 DataTable with TanStack Table
**File:** `resources/js/components/ui/data-table.tsx`

**Installation:**
```bash
npm install @tanstack/react-table
```

**Features:**
- Powered by TanStack Table (via shadcn pattern)
- Sortable columns
- Pagination (Inertia-compatible)
- Row actions dropdown
- Loading skeleton
- Empty state
- Responsive design

**Implementation:**
```typescript
import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from '@tanstack/react-table'

interface DataTableProps<TData, TValue> {
  data: TData[]
  columns: ColumnDef<TData, TValue>[]
  pagination?: {
    currentPage: number
    lastPage: number
    onNext: () => void
    onPrev: () => void
  }
}

export function DataTable<TData, TValue>({
  data,
  columns,
  pagination,
}: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  })

  return (
    <div className="space-y-4">
      <Table>
        <TableHeader>
          {table.getHeaderGroups().map((headerGroup) => (
            <TableRow key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <TableHead key={header.id}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </TableHead>
              ))}
              <TableHead>Aksi</TableHead>
            </TableRow>
          ))}
        </TableHeader>
        <TableBody>
          {table.getRowModel().rows?.length ? (
            table.getRowModel().rows.map((row) => (
              <TableRow key={row.id}>
                {row.getVisibleCells().map((cell) => (
                  <TableCell key={cell.id}>
                    {flexRender(
                      cell.column.columnDef.cell,
                      cell.getContext()
                    )}
                  </TableCell>
                ))}
                <TableCell>
                  <ActionMenu item={row.original} />
                </TableCell>
              </TableRow>
            ))
          ) : null}
        </TableBody>
      </Table>
      {pagination && <Pagination {...pagination} />}
    </div>
  )
}
```

#### 1.2 StatusBadge Component
**File:** `resources/js/components/ui/status-badge.tsx`

**Status Mappings:**
```typescript
const statusConfig = {
  member: {
    active: { label: 'Aktif', color: 'bg-green-100 text-green-800' },
    inactive: { label: 'Tidak Aktif', color: 'bg-gray-100 text-gray-800' },
    pending: { label: 'Menunggu', color: 'bg-yellow-100 text-yellow-800' },
  },
  savings: {
    active: { label: 'Aktif', color: 'bg-green-100 text-green-800' },
    dormant: { label: 'Dormant', color: 'bg-yellow-100 text-yellow-800' },
    closed: { label: 'Tutup', color: 'bg-red-100 text-red-800' },
    frozen: { label: 'Dibekukan', color: 'bg-blue-100 text-blue-800' },
  },
  financing: {
    proposed: { label: 'Diajukan', color: 'bg-yellow-100 text-yellow-800' },
    approved: { label: 'Disetujui', color: 'bg-blue-100 text-blue-800' },
    active: { label: 'Aktif', color: 'bg-green-100 text-green-800' },
    paid: { label: 'Lunas', color: 'bg-green-100 text-green-800' },
    rejected: { label: 'Ditolak', color: 'bg-red-100 text-red-800' },
  },
}
```

#### 1.3 Form Components
- `DateInput` - HTML5 date picker
- `SelectInput` - Async dropdown for branch/product selection
- `TextareaInput` - Multiline text fields

---

### Phase 2: Member Management (Week 1-2)

#### 2.1 Members Index
**File:** `resources/js/Pages/Members/Index.tsx`

**Features:**
- Search by name, NIK, phone
- Filter by branch, status
- DataTable with TanStack Table
- Export to CSV
- Pagination

#### 2.2 Create Member
**File:** `resources/js/Pages/Members/Create.tsx`

**Fields:**
- Nama Lengkap (required)
- Email (required, unique)
- Telepon (required, format: 08xxxxxxxxxx)
- NIK (required, unique)
- Alamat (required, textarea)
- Cabang (required, select dropdown)
- Tanggal Lahir (optional, date picker)
- Jenis Kelamin (required, radio: Laki-laki/Perempuan)

#### 2.3 Show Member
**File:** `resources/js/Pages/Members/Show.tsx`

**Sections:**
- Member information card
- Accounts summary (savings count, financing count)
- Recent activity (last 5 transactions)
- Action buttons: Open Savings, Apply for Financing

#### 2.4 Edit Member
**File:** `resources/js/Pages/Members/Edit.tsx`

Reuse Create form, pre-fill with existing data

---

### Phase 3: Savings Management (Week 3)

#### 3.1 Savings Index
- Search by account number, member name
- Filter by branch, product, status
- Balance summary cards
- Quick actions: View, Deposit, Withdraw, Close

#### 3.2 Open Savings Account
- Member search (async dropdown)
- Product selection
- Initial deposit amount
- Product info display

#### 3.3 Show Savings Account
- Account details with member link
- Balance display (IDR format)
- Quick action buttons: Deposit, Withdraw
- Recharts balance history graph
- Transaction history table

#### 3.4 Transaction Modal
- Type selector (Deposit/Withdraw)
- Amount input with quick buttons [100k] [500k] [1M]
- Description textarea
- Real-time validation (available balance for withdrawal)

---

### Phase 4: Financing Management (Week 4-5)

#### 4.1 Financing Index
- Search by account number, member name
- Status tabs (Proposed, Approved, Active, Paid, Rejected)
- Summary cards
- Status-based action buttons

#### 4.2 Create Financing
- Member search dropdown
- Product selection with details
- Principal amount input
- Auto-calculation preview (margin, installment, total)
- Installment frequency (Daily, Weekly, Monthly)
- Purpose/description

#### 4.3 Show Financing
- Financing details card
- Progress bar (paid vs total)
- Status-based action modals
- Installment schedule table
- Payment history table

#### 4.4 Workflow Modals
- **Approve:** Approval notes, approved amount
- **Reject:** Rejection reason (required)
- **Disburse:** Disbursement account, method (Cash/Transfer)
- **Payment:** Payment amount, auto-allocation (principal, margin), payment date

---

### Phase 5: Branches & Polish (Week 6)

#### 5.1 Branches CRUD
- Index with table view
- Statistics per branch
- Create/Show/Edit pages

#### 5.2 Dark Mode
- Theme provider with next-themes
- Toggle in sidebar/user menu
- System preference detection
- localStorage persistence

#### 5.3 Loading States
- Skeleton components for all pages
- Inertia pending state handling

#### 5.4 Empty States
- Consistent empty state components
- Call-to-action buttons

#### 5.5 Responsive Design
- Mobile optimization
- Table horizontal scroll on small screens

#### 5.6 Accessibility
- ARIA labels
- Keyboard navigation
- Screen reader support

---

## Technical Patterns

### Data Fetching with Inertia
```typescript
// Backend
return Inertia::render('Members/Index', [
    'members' => Member::with('branch')->paginate(15),
    'branches' => Branch::all(['id', 'name']),
]);

// Frontend
interface PageProps {
  members: PaginatedData<Member>
  branches: Branch[]
}
```

### Form Submission
```typescript
import { useForm } from '@inertiajs/react'

const form = useForm({
  name: '',
  email: '',
  phone: '',
})

form.post(route('members.store'))
```

### TanStack Table Usage
```typescript
const columns: ColumnDef<Member>[] = [
  {
    accessorKey: 'name',
    header: 'Nama Anggota',
    cell: ({ row }) => row.getValue('name'),
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => <StatusBadge status={row.getValue('status')} type="member" />,
  },
]
```

### Export to CSV
```typescript
const exportToCSV = (data: any[], filename: string) => {
  const csv = convertToCSV(data)
  const blob = new Blob([csv], { type: 'text/csv' })
  const url = window.URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `${filename}.csv`
  a.click()
}
```

### Dark Mode with next-themes
```typescript
// tailwind.config.js
export default {
  darkMode: 'class',
  content: ['./resources/**/*.{js,ts,jsx,tsx}'],
}

// theme-provider.tsx
import { ThemeProvider } from 'next-themes'
import { useEffect } from 'react'

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const { theme, setTheme } = useTheme()

  useEffect(() => {
    const stored = localStorage.getItem('theme') as 'light' | 'dark' | null
    if (stored) {
      setTheme(stored)
    } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      setTheme('dark')
    }
  }, [])

  return (
    <ThemeProvider attribute="class" defaultTheme="light">
      {children}
    </ThemeProvider>
  )
}
```

---

## File Structure

### New Components
```
resources/js/components/
├── ui/
│   ├── data-table.tsx (NEW - TanStack Table)
│   ├── status-badge.tsx (NEW)
│   ├── loading-skeleton.tsx (NEW)
│   ├── empty-state.tsx (NEW)
│   └── export-button.tsx (NEW - CSV export)
├── forms/
│   ├── date-input.tsx (NEW - HTML5 date picker)
│   ├── select-input.tsx (NEW - async dropdown)
│   └── textarea-input.tsx (NEW)
├── savings/
│   ├── transaction-modal.tsx (NEW)
│   └── open-account-modal.tsx (NEW)
└── financing/
    ├── approve-modal.tsx (NEW)
    ├── reject-modal.tsx (NEW)
    ├── disburse-modal.tsx (NEW)
    └── payment-modal.tsx (NEW)
```

### Pages to Modify/Create
```
resources/js/Pages/Members/
├── Index.tsx (MODIFY - backend integration)
├── Show.tsx (MODIFY - implement layout)
├── Create.tsx (MODIFY - add form)
└── Edit.tsx (MODIFY - add form)

resources/js/Pages/Savings/
├── Index.tsx (MODIFY - backend integration)
├── Show.tsx (NEW - full page with Recharts)
└── Open.tsx (NEW - open account form)

resources/js/Pages/Financing/
├── Index.tsx (MODIFY - backend integration)
├── Show.tsx (NEW - with installments/payments)
└── Create.tsx (NEW - application form)

resources/js/Pages/Branches/
├── Index.tsx (MODIFY - table view upgrade)
├── Show.tsx (MODIFY - implement layout)
├── Create.tsx (MODIFY - add form)
└── Edit.tsx (MODIFY - add form)
```

---

## Testing Strategy

### Vitest (Component Tests)
```bash
npm install -D vitest @testing-library/react @testing-library/jest-dom
```

**Test Examples:**
```typescript
// data-table.test.tsx
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { DataTable } from '../ui/data-table'

describe('DataTable', () => {
  it('renders rows correctly', () => {
    const data = [{ name: 'John', email: 'john@example.com' }]
    const columns = [
      { header: 'Name', accessorKey: 'name' },
      { header: 'Email', accessorKey: 'email' },
    ]

    render(<DataTable data={data} columns={columns} />)

    expect(screen.getByText('John')).toBeInTheDocument()
    expect(screen.getByText('john@example.com')).toBeInTheDocument()
  })

  it('shows empty state when no data', () => {
    render(<DataTable data={[]} columns={[]} />)
    expect(screen.getByText('Tidak ada data')).toBeInTheDocument()
  })
})
```

### Playwright (E2E Tests)
```bash
npm install -D @playwright/test
npx playwright install
```

**Test Examples:**
```typescript
// e2e/members.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Member Management', () => {
  test('can create a new member', async ({ page }) => {
    await page.goto('/members/create')
    await page.fill('input[name="name"]', 'John Doe')
    await page.fill('input[name="email"]', 'john@example.com')
    await page.click('button[type="submit"]')

    await expect(page).toHaveURL(/\/members\/\d+/)
    await expect(page.locator('h1')).toContainText('John Doe')
  })

  test('can search members', async ({ page }) => {
    await page.goto('/members')
    await page.fill('input[name="search"]', 'John')
    await page.press('input[name="search"]', 'Enter')

    await expect(page.locator('table tbody tr')).toHaveCount(1)
  })
})
```

---

## Success Criteria

Each module is complete when:
- ✅ All CRUD operations work
- ✅ Forms show validation errors
- ✅ Loading skeletons display
- ✅ Empty states display
- ✅ Tables sortable/paginated
- ✅ Search/filters work
- ✅ Export to CSV works
- ✅ Responsive on mobile
- ✅ No TypeScript errors
- ✅ No console errors
- ✅ Can navigate between entities
- ✅ Backend authorization respected
- ✅ Vitest tests passing
- ✅ Playwright E2E tests passing

---

## Sprint Timeline

- **Sprint 1 (Week 1-2):** Foundation + Members
- **Sprint 2 (Week 3):** Savings
- **Sprint 3 (Week 4-5):** Financing
- **Sprint 4 (Week 6):** Branches + Dark Mode + Polish

---

## Notes

- **Language:** Indonesian for user-facing UI, English for technical terms
- **Icons:** Lucide icons throughout
- **Charts:** Recharts for graphs
- **Tables:** TanStack Table via shadcn pattern
- **Dates:** HTML5 date picker
- **Testing:** Vitest + Playwright
- **Export:** CSV functionality for all tables
- **Dark Mode:** Full support with system preference detection
