Add realistic loan amortization with currentBalance back-calculation

- Extend schema with effectiveAnnualRate, totalInterest, totalAmount
- Back-calculate paid months from currentBalance and rebuild schedule
- Allow schedule calculation from termMonths without monthlyPayment
- Handle NaN form values gracefully
- Show effective rate, total interest and total amount in UI
- Add amortization unit tests
This commit is contained in:
Matthias
2026-06-15 20:02:44 +02:00
parent 4869402d45
commit 4a1cbd105b
7 changed files with 380 additions and 20 deletions

View File

@@ -8,7 +8,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { formatAmount } from "@/lib/format";
import { LoanFormDialog } from "@/components/loans/LoanFormDialog";
import { AmortizationSchedule } from "@/components/loans/AmortizationSchedule";
import { buildSchedule, currentBalanceFromSchedule } from "@convex-lib/amortization";
import { buildSchedule } from "@convex-lib/amortization";
export function LoansPage() {
const loans = useQuery(api.loans.list);
@@ -25,9 +25,9 @@ export function LoansPage() {
startDate,
monthlyPayment: loan.monthlyPayment,
termMonths: loan.termMonths,
currentBalance: loan.currentBalance ?? undefined,
});
const balance =
loan.currentBalance ?? currentBalanceFromSchedule(schedule.schedule, startDate);
const balance = schedule.currentBalance;
return { loan, schedule, balance };
});
}, [loans]);
@@ -47,21 +47,27 @@ export function LoansPage() {
<TableHead>Gläubiger</TableHead>
<TableHead>Summe</TableHead>
<TableHead>Zins</TableHead>
<TableHead>Eff. Zins</TableHead>
<TableHead>Rate</TableHead>
<TableHead>Restschuld</TableHead>
<TableHead>Gesamtzinsen</TableHead>
<TableHead>Gesamtbetrag</TableHead>
<TableHead>Status</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{enriched.map(({ loan, balance }) => (
{enriched.map(({ loan, schedule, balance }) => (
<TableRow key={loan._id}>
<TableCell>{loan.name}</TableCell>
<TableCell>{loan.lender ?? ""}</TableCell>
<TableCell>{formatAmount(loan.principal)}</TableCell>
<TableCell>{loan.annualInterestRate.toFixed(2)} %</TableCell>
<TableCell>{loan.effectiveAnnualRate ? `${loan.effectiveAnnualRate.toFixed(2)} %` : ""}</TableCell>
<TableCell>{loan.monthlyPayment ? formatAmount(loan.monthlyPayment) : ""}</TableCell>
<TableCell>{formatAmount(balance)}</TableCell>
<TableCell>{formatAmount(schedule.totalInterest)}</TableCell>
<TableCell>{formatAmount(schedule.totalAmount)}</TableCell>
<TableCell>{loan.status}</TableCell>
<TableCell className="space-x-1">
<Button size="sm" variant="outline" onClick={() => setEditLoan(loan)}>