# Frontend Integration Guide ## Bank Transaction CSV Import UI ### Where to Add the UI Create a new page or add to an existing bank management section: - Suggested location: `/src/app/(dashboard)/finance/treasury/bank-transactions/import` - Or: Add as a tab in `/finance/treasury` page ### Component Structure ```tsx 'use client'; import { useState } from 'react'; import { Upload, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; interface ParsedTransaction { transactionDate: string; description: string; transactionType: 'DEBIT' | 'CREDIT'; amount: number; } export default function BankTransactionImportPage() { const [file, setFile] = useState(null); const [parsing, setParsing] = useState(false); const [parseError, setParseError] = useState(null); const [parsedTransactions, setParsedTransactions] = useState([]); const [importing, setImporting] = useState(false); const [importError, setImportError] = useState(null); const [bankAccountId, setBankAccountId] = useState(''); // Step 1: Parse CSV const handleFileParse = async (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (!selectedFile) return; setFile(selectedFile); setParsing(true); setParseError(null); try { const formData = new FormData(); formData.append('file', selectedFile); const response = await fetch('/api/bank-transactions/parse-csv', { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json(); setParseError(error.error || 'Failed to parse CSV'); setParsedTransactions([]); return; } const data = await response.json(); setParsedTransactions(data.transactions); } catch (error) { setParseError( error instanceof Error ? error.message : 'Error parsing CSV file' ); setParsedTransactions([]); } finally { setParsing(false); } }; // Step 2: Import transactions const handleImport = async () => { if (!bankAccountId) { setImportError('Please select a bank account'); return; } if (parsedTransactions.length === 0) { setImportError('No transactions to import'); return; } setImporting(true); setImportError(null); try { const response = await fetch('/api/bank-transactions/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bankAccountId, transactions: parsedTransactions, }), }); if (!response.ok) { const error = await response.json(); setImportError(error.error || 'Failed to import transactions'); return; } const data = await response.json(); // Success - reset form and show success message setFile(null); setParsedTransactions([]); setBankAccountId(''); // Show success toast/notification console.log(`Successfully imported ${data.imported} transactions`); } catch (error) { setImportError( error instanceof Error ? error.message : 'Error importing transactions' ); } finally { setImporting(false); } }; return (
{/* File Upload Section */}

Upload Bank Transactions

Supported formats: CSV with columns for Date, Description, and Amount (or Debit/Credit)

{parsing && (
Parsing CSV...
)} {parseError && (

Error parsing CSV

{parseError}

)}
{/* Preview Section */} {parsedTransactions.length > 0 && (

Preview: {parsedTransactions.length} Transactions

{parsedTransactions.map((tx, idx) => ( ))}
Date Description Type Amount
{tx.transactionDate} {tx.description.length > 50 ? tx.description.substring(0, 50) + '...' : tx.description} {tx.transactionType} ${tx.amount.toFixed(2)}
{/* Bank Account Selection */}
{importError && (

Import Error

{importError}

)}
)}
); } ``` --- ## Payment Recording UI ### Already Implemented The payment recording modal is already built into the AR Invoice Detail page: **Location:** `/src/app/(dashboard)/finance/ar/invoice/[invoiceId]/page.tsx` **How to Use:** 1. Navigate to any AR invoice 2. Click "Record Payment" button in Actions section 3. Fill in the payment form: - Amount (required) - Date (required) - Method (required: ACH, WIRE, CHECK, etc.) 4. Click "Record Payment" 5. Page refreshes to show updated invoice status **Direct Navigation:** Users can also navigate directly to the payment form: ``` /finance/ar/invoice/{invoiceId}?action=payment ``` This auto-opens the payment modal. --- ## Bank Account Selection Component ### Reusable Component for Bank Account Dropdown ```tsx import { useState, useEffect } from 'react'; interface BankAccount { id: string; accountName: string; accountNumberMasked: string; bankName: string; currentBalance: number; } interface BankAccountSelectProps { value: string; onChange: (value: string) => void; required?: boolean; disabled?: boolean; } export function BankAccountSelect({ value, onChange, required = false, disabled = false, }: BankAccountSelectProps) { const [accounts, setAccounts] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchAccounts = async () => { try { const response = await fetch('/api/bank-accounts'); if (response.ok) { const data = await response.json(); setAccounts(data); } } catch (error) { console.error('Failed to fetch bank accounts', error); } finally { setLoading(false); } }; fetchAccounts(); }, []); return ( ); } ``` --- ## Form Validation Helpers ### Utility Functions for Form Validation ```tsx export function validateAmount(value: string | number): string | null { const amount = parseFloat(String(value)); if (isNaN(amount)) { return 'Amount must be a valid number'; } if (amount <= 0) { return 'Amount must be greater than 0'; } return null; } export function validateDate(value: string): string | null { const date = new Date(value); if (isNaN(date.getTime())) { return 'Invalid date format'; } // Don't allow future dates if (date > new Date()) { return 'Date cannot be in the future'; } return null; } export function validateFileSize( file: File, maxSizeMB: number = 10 ): string | null { const maxBytes = maxSizeMB * 1024 * 1024; if (file.size > maxBytes) { return `File size must be less than ${maxSizeMB}MB`; } return null; } export function validateCSVFile(file: File): string | null { if (!file.name.endsWith('.csv')) { return 'File must be a CSV file (.csv)'; } const sizeError = validateFileSize(file, 10); if (sizeError) return sizeError; return null; } ``` --- ## API Integration Patterns ### Fetch Bank Accounts ```tsx async function fetchBankAccounts() { const response = await fetch('/api/bank-accounts'); if (!response.ok) throw new Error('Failed to fetch accounts'); return response.json(); } ``` ### Parse CSV ```tsx async function parseCSV(file: File) { const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/bank-transactions/parse-csv', { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json(); throw new Error(error.error); } return response.json(); } ``` ### Import Transactions ```tsx async function importTransactions( bankAccountId: string, transactions: any[] ) { const response = await fetch('/api/bank-transactions/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bankAccountId, transactions, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error); } return response.json(); } ``` ### Record Payment ```tsx async function recordPayment( customerId: string, invoiceId: string, paymentData: { paymentDate: string; paymentMethod: string; paymentAmount: number; } ) { const response = await fetch('/api/payments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customerId, invoiceId, ...paymentData, paymentAllocations: [ { invoiceId, allocatedAmount: paymentData.paymentAmount, }, ], }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error); } return response.json(); } ``` --- ## Toast/Notification Integration Add success/error notifications to your pages: ```tsx import { useState } from 'react'; function useNotification() { const [notification, setNotification] = useState<{ type: 'success' | 'error' | 'info'; message: string; } | null>(null); return { notification, showSuccess: (message: string) => { setNotification({ type: 'success', message }); setTimeout(() => setNotification(null), 3000); }, showError: (message: string) => { setNotification({ type: 'error', message }); setTimeout(() => setNotification(null), 5000); }, showInfo: (message: string) => { setNotification({ type: 'info', message }); setTimeout(() => setNotification(null), 3000); }, }; } // Usage in component const { notification, showSuccess } = useNotification(); const handleImport = async () => { try { const result = await importTransactions(bankAccountId, transactions); showSuccess(`Successfully imported ${result.imported} transactions`); } catch (error) { showError(error instanceof Error ? error.message : 'Import failed'); } }; ``` --- ## Testing Checklist - [ ] CSV upload accepts .csv files - [ ] CSV parse shows correct transaction count - [ ] CSV preview shows transactions with correct types and amounts - [ ] Bank account dropdown populates from API - [ ] Import button is disabled until account is selected - [ ] Import shows loading state during submission - [ ] Import success message appears - [ ] Payment form validates amount > 0 - [ ] Payment form validates date not in future - [ ] Payment method dropdown has all options - [ ] Payment submit shows loading state - [ ] Payment success refreshes invoice - [ ] Error messages display with helpful text - [ ] Modal closes on cancel or success - [ ] Query parameter ?action=payment opens modal --- ## Performance Considerations 1. **Batch Imports:** All transactions imported in single API call 2. **File Size:** Limit CSV files to 10MB (handles 50k+ transactions) 3. **Preview Table:** Paginate if more than 1000 transactions 4. **Debouncing:** Debounce file input changes 5. **Caching:** Cache bank accounts list with SWR or React Query --- ## Accessibility Notes - Form labels properly associated with inputs - Error messages clearly displayed in red - Loading states show spinner with text - Keyboard navigation: Tab through form - Screen reader support: ARIA labels on all inputs