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

@@ -27,6 +27,7 @@ export function AmortizationSchedule({
startDate: new Date(loan.startDate),
monthlyPayment: loan.monthlyPayment,
termMonths: loan.termMonths,
currentBalance: loan.currentBalance ?? undefined,
});
}, [loan]);
@@ -71,8 +72,15 @@ export function AmortizationSchedule({
</TableBody>
</Table>
<p className="text-sm text-muted-foreground">
Gesamtzinsen: {formatAmount(scheduleResult.totalInterest)} · Enddatum:{" "}
{formatDate(scheduleResult.payoffDate.toISOString().slice(0, 10))}
Gesamtzinsen: {formatAmount(scheduleResult.totalInterest)} · Gesamtbetrag:{" "}
{formatAmount(scheduleResult.totalAmount)} · Enddatum:{" "}
{formatDate(
Number.isNaN(scheduleResult.payoffDate.getTime())
? undefined
: scheduleResult.payoffDate.toISOString().slice(0, 10),
)}
{loan.effectiveAnnualRate !== undefined &&
` · Effektivzins: ${loan.effectiveAnnualRate.toFixed(2)} %`}
</p>
</DialogContent>
</Dialog>

View File

@@ -29,10 +29,13 @@ export function LoanFormDialog({
lender: "",
principal: 10000,
annualInterestRate: 3.5,
effectiveAnnualRate: undefined as number | undefined,
monthlyPayment: undefined as number | undefined,
termMonths: undefined as number | undefined,
startDate: new Date().toISOString().slice(0, 10),
currentBalance: undefined as number | undefined,
totalInterest: undefined as number | undefined,
totalAmount: undefined as number | undefined,
status: "aktiv" as "aktiv" | "abbezahlt" | "pausiert",
notes: "",
},
@@ -45,10 +48,13 @@ export function LoanFormDialog({
lender: loan.lender ?? "",
principal: loan.principal,
annualInterestRate: loan.annualInterestRate,
effectiveAnnualRate: loan.effectiveAnnualRate,
monthlyPayment: loan.monthlyPayment,
termMonths: loan.termMonths,
startDate: loan.startDate,
currentBalance: loan.currentBalance,
totalInterest: loan.totalInterest,
totalAmount: loan.totalAmount,
status: loan.status,
notes: loan.notes ?? "",
});
@@ -56,16 +62,30 @@ export function LoanFormDialog({
}, [loan, form]);
const onSubmit = form.handleSubmit(async (values) => {
const monthlyPayment = Number.isNaN(values.monthlyPayment ?? 0) ? undefined : values.monthlyPayment;
const termMonths = Number.isNaN(values.termMonths ?? 0) ? undefined : values.termMonths;
const currentBalance = Number.isNaN(values.currentBalance ?? 0) ? undefined : values.currentBalance;
const effectiveAnnualRate = Number.isNaN(values.effectiveAnnualRate ?? 0) ? undefined : values.effectiveAnnualRate;
const totalInterest = Number.isNaN(values.totalInterest ?? 0) ? undefined : values.totalInterest;
const totalAmount = Number.isNaN(values.totalAmount ?? 0) ? undefined : values.totalAmount;
if (monthlyPayment === undefined && termMonths === undefined) {
toast.error("Bitte Monatsrate oder Laufzeit angeben");
return;
}
try {
const payload = {
name: values.name,
lender: values.lender || undefined,
principal: values.principal,
annualInterestRate: values.annualInterestRate,
monthlyPayment: values.monthlyPayment,
termMonths: values.termMonths,
effectiveAnnualRate,
monthlyPayment,
termMonths,
startDate: values.startDate,
currentBalance: values.currentBalance,
currentBalance,
totalInterest,
totalAmount,
status: values.status,
notes: values.notes || undefined,
};
@@ -118,13 +138,19 @@ export function LoanFormDialog({
<Label>Jahreszins (%)</Label>
<Input type="number" step="0.01" {...form.register("annualInterestRate", { valueAsNumber: true })} />
</div>
<div>
<Label>Effektiver Jahreszins (%)</Label>
<Input type="number" step="0.01" {...form.register("effectiveAnnualRate", { valueAsNumber: true })} />
</div>
<div>
<Label>Monatsrate</Label>
<Input type="number" step="0.01" {...form.register("monthlyPayment", { valueAsNumber: true })} />
<p className="text-xs text-muted-foreground mt-1">Rate oder Laufzeit angeben</p>
</div>
<div>
<Label>Laufzeit (Monate)</Label>
<Input type="number" {...form.register("termMonths", { valueAsNumber: true })} />
<p className="text-xs text-muted-foreground mt-1">Rate oder Laufzeit angeben</p>
</div>
<div>
<Label>Startdatum</Label>
@@ -134,6 +160,14 @@ export function LoanFormDialog({
<Label>Aktuelle Restschuld (optional)</Label>
<Input type="number" step="0.01" {...form.register("currentBalance", { valueAsNumber: true })} />
</div>
<div>
<Label>Gesamtzinsen lt. Vertrag (optional)</Label>
<Input type="number" step="0.01" {...form.register("totalInterest", { valueAsNumber: true })} />
</div>
<div>
<Label>Gesamtbetrag lt. Vertrag (optional)</Label>
<Input type="number" step="0.01" {...form.register("totalAmount", { valueAsNumber: true })} />
</div>
<div className="sm:col-span-2">
<Label>Notiz</Label>
<Input {...form.register("notes")} />