diff options
Diffstat (limited to 'frontend/src/app/(main)/loans/page.tsx')
-rw-r--r-- | frontend/src/app/(main)/loans/page.tsx | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/frontend/src/app/(main)/loans/page.tsx b/frontend/src/app/(main)/loans/page.tsx new file mode 100644 index 0000000..07c1428 --- /dev/null +++ b/frontend/src/app/(main)/loans/page.tsx @@ -0,0 +1,279 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '@/components/ui/table'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { loanApi, Loan, LoanInput } from '@/lib/api'; + +export default function LoansPage() { + const queryClient = useQueryClient(); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [selectedLoan, setSelectedLoan] = useState<Loan | null>(null); + + // Form state + const [loanName, setLoanName] = useState(''); + const [originalAmount, setOriginalAmount] = useState(''); + const [currentBalance, setCurrentBalance] = useState(''); + const [interestRate, setInterestRate] = useState(''); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + const [formError, setFormError] = useState(''); + + // Query loans + const { data, isLoading, error } = useQuery({ + queryKey: ['loans'], + queryFn: async () => { + const response = await loanApi.getLoans(); + return response.loans as Loan[]; + } + }); + + // Create loan mutation + const createLoanMutation = useMutation({ + mutationFn: (loanData: LoanInput) => loanApi.createLoan(loanData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['loans'] }); + resetForm(); + setIsAddDialogOpen(false); + }, + onError: (error: Error) => { + setFormError(error.message); + } + }); + + // Delete loan mutation + const deleteLoanMutation = useMutation({ + mutationFn: (id: number) => loanApi.deleteLoan(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['loans'] }); + } + }); + + const resetForm = () => { + setLoanName(''); + setOriginalAmount(''); + setCurrentBalance(''); + setInterestRate(''); + setStartDate(''); + setEndDate(''); + setFormError(''); + setSelectedLoan(null); + }; + + const handleCreateLoan = (e: React.FormEvent) => { + e.preventDefault(); + setFormError(''); + + try { + // Validate inputs + if (!loanName || !originalAmount || !currentBalance || !startDate || !endDate) { + setFormError('Please fill in all required fields'); + return; + } + + const loanData: LoanInput = { + name: loanName, + originalAmount: Number(originalAmount) * 100, // Convert to cents + currentBalance: Number(currentBalance) * 100, // Convert to cents + interestRate: Number(interestRate) || 0, + startDate, + endDate, + }; + + createLoanMutation.mutate(loanData); + } catch (err: any) { + setFormError(err.message || 'Error creating loan'); + } + }; + + const handleDeleteLoan = (id: number) => { + if (window.confirm('Are you sure you want to delete this loan?')) { + deleteLoanMutation.mutate(id); + } + }; + + // Format currency (convert cents to dollars) + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(amount / 100); + }; + + // Format date + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString(); + }; + + if (isLoading) { + return <div>Loading loans...</div>; + } + + if (error) { + return <div>Error loading loans: {error.toString()}</div>; + } + + return ( + <div className="space-y-6"> + <div className="flex items-center justify-between"> + <h1 className="text-3xl font-bold">Loans</h1> + <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}> + <DialogTrigger asChild> + <Button onClick={resetForm}>Add New Loan</Button> + </DialogTrigger> + <DialogContent> + <DialogHeader> + <DialogTitle>Add New Loan</DialogTitle> + </DialogHeader> + <form onSubmit={handleCreateLoan} className="space-y-4"> + <div className="space-y-2"> + <Label htmlFor="name">Loan Name</Label> + <Input + id="name" + placeholder="e.g., Car Loan" + value={loanName} + onChange={(e) => setLoanName(e.target.value)} + required + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="originalAmount">Original Amount ($)</Label> + <Input + id="originalAmount" + type="number" + step="0.01" + placeholder="10000.00" + value={originalAmount} + onChange={(e) => setOriginalAmount(e.target.value)} + required + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="currentBalance">Current Balance ($)</Label> + <Input + id="currentBalance" + type="number" + step="0.01" + placeholder="9500.00" + value={currentBalance} + onChange={(e) => setCurrentBalance(e.target.value)} + required + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="interestRate">Interest Rate (%)</Label> + <Input + id="interestRate" + type="number" + step="0.01" + placeholder="5.25" + value={interestRate} + onChange={(e) => setInterestRate(e.target.value)} + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="startDate">Start Date</Label> + <Input + id="startDate" + type="date" + value={startDate} + onChange={(e) => setStartDate(e.target.value)} + required + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="endDate">End Date</Label> + <Input + id="endDate" + type="date" + value={endDate} + onChange={(e) => setEndDate(e.target.value)} + required + /> + </div> + + {formError && ( + <div className="text-sm text-red-500"> + {formError} + </div> + )} + + <div className="flex justify-end space-x-2"> + <Button type="button" variant="outline" onClick={() => setIsAddDialogOpen(false)}> + Cancel + </Button> + <Button type="submit" disabled={createLoanMutation.isPending}> + {createLoanMutation.isPending ? 'Saving...' : 'Save Loan'} + </Button> + </div> + </form> + </DialogContent> + </Dialog> + </div> + + {data && data.length > 0 ? ( + <Card> + <CardHeader> + <CardTitle>Your Loans</CardTitle> + </CardHeader> + <CardContent> + <Table> + <TableHeader> + <TableRow> + <TableHead>Name</TableHead> + <TableHead>Original Amount</TableHead> + <TableHead>Current Balance</TableHead> + <TableHead>Interest Rate</TableHead> + <TableHead>Start Date</TableHead> + <TableHead>End Date</TableHead> + <TableHead>Actions</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {data.map((loan) => ( + <TableRow key={loan.ID}> + <TableCell className="font-medium">{loan.Name}</TableCell> + <TableCell>{formatCurrency(loan.OriginalAmount)}</TableCell> + <TableCell>{formatCurrency(loan.CurrentBalance)}</TableCell> + <TableCell>{loan.InterestRate}%</TableCell> + <TableCell>{formatDate(loan.StartDate)}</TableCell> + <TableCell>{formatDate(loan.EndDate)}</TableCell> + <TableCell> + <div className="flex items-center space-x-2"> + <Button + variant="destructive" + size="sm" + onClick={() => handleDeleteLoan(loan.ID)} + disabled={deleteLoanMutation.isPending} + > + Delete + </Button> + </div> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </CardContent> + </Card> + ) : ( + <Card> + <CardContent className="flex flex-col items-center justify-center p-10"> + <p className="text-muted-foreground mb-4">You don't have any loans yet.</p> + <Button onClick={() => setIsAddDialogOpen(true)}>Add Your First Loan</Button> + </CardContent> + </Card> + )} + </div> + ); +}
\ No newline at end of file |