diff --git a/app/api/households/[id]/debt-payoff/route.ts b/app/api/households/[id]/debt-payoff/route.ts new file mode 100644 index 0000000..f79b502 --- /dev/null +++ b/app/api/households/[id]/debt-payoff/route.ts @@ -0,0 +1,9 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST( + req: NextRequest, + { params }: { params: { id: string } } +) { + return proxyRequest(req, `households/${params.id}/debt-payoff`); +} diff --git a/app/settings/households/page.tsx b/app/settings/households/page.tsx index 6b2a4c4..66c316a 100644 --- a/app/settings/households/page.tsx +++ b/app/settings/households/page.tsx @@ -171,6 +171,40 @@ type MoneyDatePromptResponse = { prompts: MoneyDatePrompt[]; }; +type DebtPayoffResult = { + strategy: "avalanche" | "snowball" | "custom"; + monthlyExtraPayment: number; + monthlyPayment: number; + totalStartingBalance: number; + totalInterestPaid: number; + payoffMonths: number; + payoffYears: number; + contributionSplit: { + splitMode: "equal" | "income_weighted" | "custom"; + minePercent: number; + yoursPercent: number; + mineAmount: number; + yoursAmount: number; + rationale: string; + }; + orderedDebts: Array<{ + name: string; + startingBalance: number; + annualPercentageRate: number; + minimumPayment: number; + payoffOrder: number; + }>; + timeline: Array<{ + month: number; + targetDebt: string; + totalRemainingBalance: number; + interestPaid: number; + principalPaid: number; + paidOffDebts: string[]; + }>; + recommendations: string[]; +}; + const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", @@ -228,6 +262,20 @@ export default function HouseholdSettingsPage() { }); const [fairSplitResult, setFairSplitResult] = useState(null); const [fairSplitStatus, setFairSplitStatus] = useState(""); + const [debtDraft, setDebtDraft] = useState({ + strategy: "avalanche" as "avalanche" | "snowball" | "custom", + monthlyExtraPayment: "300", + splitMode: "income_weighted" as "equal" | "income_weighted" | "custom", + mineMonthlyIncome: "", + yoursMonthlyIncome: "", + customMinePercent: "50", + debts: [ + { name: "Credit card", balance: "", annualPercentageRate: "", minimumPayment: "", priority: "1" }, + { name: "Personal loan", balance: "", annualPercentageRate: "", minimumPayment: "", priority: "2" }, + ], + }); + const [debtPayoffResult, setDebtPayoffResult] = useState(null); + const [debtPayoffStatus, setDebtPayoffStatus] = useState(""); const [privacyStatus, setPrivacyStatus] = useState(""); const [moneyDatePrompts, setMoneyDatePrompts] = useState(null); const [moneyDateStatus, setMoneyDateStatus] = useState(""); @@ -451,6 +499,62 @@ export default function HouseholdSettingsPage() { setFairSplitStatus(""); }; + const updateDebtDraft = (index: number, field: "name" | "balance" | "annualPercentageRate" | "minimumPayment" | "priority", value: string) => { + setDebtDraft((prev) => ({ + ...prev, + debts: prev.debts.map((debt, i) => i === index ? { ...debt, [field]: value } : debt), + })); + }; + + const addDebtDraftRow = () => { + setDebtDraft((prev) => ({ + ...prev, + debts: [ + ...prev.debts, + { name: `Debt ${prev.debts.length + 1}`, balance: "", annualPercentageRate: "", minimumPayment: "", priority: String(prev.debts.length + 1) }, + ], + })); + }; + + const calculateDebtPayoff = async (event: FormEvent) => { + event.preventDefault(); + if (!selectedId) return; + const debts = debtDraft.debts + .filter((debt) => debt.name.trim() && debt.balance && debt.minimumPayment) + .map((debt) => ({ + name: debt.name, + balance: Number.parseFloat(debt.balance), + annualPercentageRate: Number.parseFloat(debt.annualPercentageRate || "0"), + minimumPayment: Number.parseFloat(debt.minimumPayment), + priority: Number.parseInt(debt.priority || "0", 10), + })); + if (!debts.length) { + setDebtPayoffStatus("Add at least one debt with balance and minimum payment."); + return; + } + + setDebtPayoffStatus("Calculating debt payoff plan..."); + const res = await apiFetch(`/api/households/${selectedId}/debt-payoff`, { + method: "POST", + body: JSON.stringify({ + strategy: debtDraft.strategy, + monthlyExtraPayment: Number.parseFloat(debtDraft.monthlyExtraPayment || "0"), + splitMode: debtDraft.splitMode, + mineMonthlyIncome: Number.parseFloat(debtDraft.mineMonthlyIncome || "0"), + yoursMonthlyIncome: Number.parseFloat(debtDraft.yoursMonthlyIncome || "0"), + customMinePercent: debtDraft.splitMode === "custom" ? Number.parseFloat(debtDraft.customMinePercent) : undefined, + debts, + }), + }); + if (res.error) { + setDebtPayoffResult(null); + setDebtPayoffStatus(res.error.message ?? "Unable to calculate debt payoff plan."); + return; + } + setDebtPayoffResult(res.data ?? null); + setDebtPayoffStatus(""); + }; + const updateMemberAccess = async (member: HouseholdMember, payload: { role?: string; status?: string }) => { if (!selectedId) return; const res = await apiFetch(`/api/households/${selectedId}/members/${member.id}`, { @@ -794,6 +898,145 @@ export default function HouseholdSettingsPage() { +
+
+
+

Debt payoff teamwork

+

Plan a shared payoff order and contribution split

+

+ Compare avalanche, snowball, or custom priority and agree how much each partner contributes monthly. +

+ {debtPayoffResult ? ( +
+
+

Payoff time

+

{debtPayoffResult.payoffMonths} months

+

{debtPayoffResult.payoffYears} years

+
+
+

Interest

+

{formatMoney(debtPayoffResult.totalInterestPaid)}

+

{formatMoney(debtPayoffResult.monthlyPayment)} monthly plan

+
+
+

Mine

+

{formatMoney(debtPayoffResult.contributionSplit.mineAmount)}

+

{debtPayoffResult.contributionSplit.minePercent}% monthly share

+
+
+

Yours

+

{formatMoney(debtPayoffResult.contributionSplit.yoursAmount)}

+

{debtPayoffResult.contributionSplit.yoursPercent}% monthly share

+
+
+

Payoff order

+
+ {debtPayoffResult.orderedDebts.map((debt) => ( +
+ {debt.payoffOrder}. {debt.name} + {debt.annualPercentageRate}% APR ยท {formatMoney(debt.startingBalance)} +
+ ))} +
+
+
+ ) : null} +
+
+
+ + setDebtDraft((prev) => ({ ...prev, monthlyExtraPayment: event.target.value }))} + placeholder="Extra monthly payment" + className={inputClass} + /> + + setDebtDraft((prev) => ({ ...prev, mineMonthlyIncome: event.target.value }))} + placeholder="Mine monthly income" + className={inputClass} + /> + setDebtDraft((prev) => ({ ...prev, yoursMonthlyIncome: event.target.value }))} + placeholder="Yours monthly income" + className={inputClass} + /> + {debtDraft.splitMode === "custom" ? ( + setDebtDraft((prev) => ({ ...prev, customMinePercent: event.target.value }))} + placeholder="Mine percent" + className={inputClass} + /> + ) : null} +
+ +
+ {debtDraft.debts.map((debt, index) => ( +
+ updateDebtDraft(index, "name", event.target.value)} placeholder="Debt name" className={inputClass} /> + updateDebtDraft(index, "balance", event.target.value)} placeholder="Balance" className={inputClass} /> + updateDebtDraft(index, "annualPercentageRate", event.target.value)} placeholder="APR %" className={inputClass} /> + updateDebtDraft(index, "minimumPayment", event.target.value)} placeholder="Minimum" className={inputClass} /> + updateDebtDraft(index, "priority", event.target.value)} placeholder="Order" className={inputClass} /> +
+ ))} +
+
+ + +
+ {debtPayoffStatus ?

{debtPayoffStatus}

: null} + {debtPayoffResult?.recommendations.length ? ( +
+

Team next steps

+
+ {debtPayoffResult.recommendations.map((recommendation) => ( +

{recommendation}

+ ))} +
+
+ ) : null} +
+
+
+