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:
@@ -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>
|
||||
|
||||
@@ -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")} />
|
||||
|
||||
Reference in New Issue
Block a user