initial commit
This commit is contained in:
607
FRONTEND_INTEGRATION_GUIDE.md
Normal file
607
FRONTEND_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,607 @@
|
||||
# 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<File | null>(null);
|
||||
const [parsing, setParsing] = useState(false);
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
const [parsedTransactions, setParsedTransactions] = useState<ParsedTransaction[]>([]);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
const [bankAccountId, setBankAccountId] = useState('');
|
||||
|
||||
// Step 1: Parse CSV
|
||||
const handleFileParse = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{/* File Upload Section */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
||||
Upload Bank Transactions
|
||||
</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select CSV File
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv"
|
||||
onChange={handleFileParse}
|
||||
disabled={parsing}
|
||||
className="block w-full text-sm text-gray-500
|
||||
file:mr-4 file:py-2 file:px-4 file:rounded-lg
|
||||
file:border-0 file:text-sm file:font-semibold
|
||||
file:bg-blue-50 file:text-blue-700
|
||||
hover:file:bg-blue-100"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Supported formats: CSV with columns for Date, Description, and Amount
|
||||
(or Debit/Credit)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{parsing && (
|
||||
<div className="flex items-center gap-2 text-blue-600 mb-4">
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
Parsing CSV...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{parseError && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4">
|
||||
<p className="font-medium">Error parsing CSV</p>
|
||||
<p className="text-sm">{parseError}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Preview Section */}
|
||||
{parsedTransactions.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
||||
Preview: {parsedTransactions.length} Transactions
|
||||
</h2>
|
||||
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-semibold text-gray-700">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-gray-700">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-semibold text-gray-700">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right font-semibold text-gray-700">
|
||||
Amount
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{parsedTransactions.map((tx, idx) => (
|
||||
<tr key={idx} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-gray-900">{tx.transactionDate}</td>
|
||||
<td className="px-4 py-3 text-gray-700">
|
||||
{tx.description.length > 50
|
||||
? tx.description.substring(0, 50) + '...'
|
||||
: tx.description}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-semibold ${
|
||||
tx.transactionType === 'DEBIT'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}
|
||||
>
|
||||
{tx.transactionType}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-medium text-gray-900">
|
||||
${tx.amount.toFixed(2)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Bank Account Selection */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Bank Account *
|
||||
</label>
|
||||
<select
|
||||
value={bankAccountId}
|
||||
onChange={(e) => setBankAccountId(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Choose a bank account...</option>
|
||||
<option value="account-id-1">Checking - 4567</option>
|
||||
<option value="account-id-2">Savings - 8901</option>
|
||||
{/* Fetch these from /api/bank-accounts */}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{importError && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4">
|
||||
<p className="font-medium">Import Error</p>
|
||||
<p className="text-sm">{importError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
setParsedTransactions([]);
|
||||
setParseError(null);
|
||||
}}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 font-semibold rounded-lg
|
||||
hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={importing || !bankAccountId}
|
||||
className="px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg
|
||||
hover:bg-blue-700 transition-colors disabled:opacity-50
|
||||
flex items-center gap-2"
|
||||
>
|
||||
{importing && <Loader2 size={16} className="animate-spin" />}
|
||||
{importing ? 'Importing...' : `Import ${parsedTransactions.length} Transactions`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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<BankAccount[]>([]);
|
||||
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 (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={disabled || loading}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">
|
||||
{loading ? 'Loading accounts...' : 'Select a bank account...'}
|
||||
</option>
|
||||
{accounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.accountName} • {account.accountNumberMasked} •{' '}
|
||||
{account.bankName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user