From c1ff8a44b8d6436c5455ed5ffe260c95d6d70ba1 Mon Sep 17 00:00:00 2001 From: MOHAN Date: Thu, 16 Jul 2026 23:57:17 +0530 Subject: [PATCH] Add future planning scenarios UI --- .../households/[id]/future-scenarios/route.ts | 9 + app/settings/households/page.tsx | 170 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 app/api/households/[id]/future-scenarios/route.ts diff --git a/app/api/households/[id]/future-scenarios/route.ts b/app/api/households/[id]/future-scenarios/route.ts new file mode 100644 index 0000000..f57c529 --- /dev/null +++ b/app/api/households/[id]/future-scenarios/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}/future-scenarios`); +} diff --git a/app/settings/households/page.tsx b/app/settings/households/page.tsx index 66c316a..658fed8 100644 --- a/app/settings/households/page.tsx +++ b/app/settings/households/page.tsx @@ -205,6 +205,31 @@ type DebtPayoffResult = { recommendations: string[]; }; +type FutureScenarioResult = { + name: string; + type: "goal" | "net_worth" | "income_change" | "expense_change"; + horizonMonths: number; + startingBalance: number; + finalBalance: number; + targetAmount: number | null; + targetReachedMonth: number | null; + monthlyNetChange: number; + totalContributions: number; + eventTotal: number; + projectedGrowth: number; + milestones: Array<{ month: number; label: string; balance: number }>; + recommendation: string; +}; + +type FutureScenarioResponse = { + generatedAt: string; + scenarios: FutureScenarioResult[]; + comparison: { + bestFinalBalance: FutureScenarioResult; + earliestTarget: FutureScenarioResult | null; + }; +}; + const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", @@ -276,6 +301,14 @@ export default function HouseholdSettingsPage() { }); const [debtPayoffResult, setDebtPayoffResult] = useState(null); const [debtPayoffStatus, setDebtPayoffStatus] = useState(""); + const [futureDraft, setFutureDraft] = useState({ + scenarios: [ + { name: "Shared milestone", type: "goal", startingBalance: "", monthlyContribution: "", monthlyIncome: "", monthlyExpenses: "", targetAmount: "", horizonMonths: "36", annualGrowthRate: "0" }, + { name: "Stretch plan", type: "net_worth", startingBalance: "", monthlyContribution: "", monthlyIncome: "", monthlyExpenses: "", targetAmount: "", horizonMonths: "36", annualGrowthRate: "3" }, + ], + }); + const [futureResult, setFutureResult] = useState(null); + const [futureStatus, setFutureStatus] = useState(""); const [privacyStatus, setPrivacyStatus] = useState(""); const [moneyDatePrompts, setMoneyDatePrompts] = useState(null); const [moneyDateStatus, setMoneyDateStatus] = useState(""); @@ -555,6 +588,62 @@ export default function HouseholdSettingsPage() { setDebtPayoffStatus(""); }; + const updateFutureScenario = ( + index: number, + field: "name" | "type" | "startingBalance" | "monthlyContribution" | "monthlyIncome" | "monthlyExpenses" | "targetAmount" | "horizonMonths" | "annualGrowthRate", + value: string, + ) => { + setFutureDraft((prev) => ({ + ...prev, + scenarios: prev.scenarios.map((scenario, i) => i === index ? { ...scenario, [field]: value } : scenario), + })); + }; + + const addFutureScenario = () => { + setFutureDraft((prev) => ({ + ...prev, + scenarios: [ + ...prev.scenarios, + { name: `Scenario ${prev.scenarios.length + 1}`, type: "goal", startingBalance: "", monthlyContribution: "", monthlyIncome: "", monthlyExpenses: "", targetAmount: "", horizonMonths: "36", annualGrowthRate: "0" }, + ], + })); + }; + + const calculateFutureScenarios = async (event: FormEvent) => { + event.preventDefault(); + if (!selectedId) return; + const scenarios = futureDraft.scenarios + .filter((scenario) => scenario.name.trim()) + .map((scenario) => ({ + name: scenario.name, + type: scenario.type, + startingBalance: Number.parseFloat(scenario.startingBalance || "0"), + monthlyContribution: Number.parseFloat(scenario.monthlyContribution || "0"), + monthlyIncome: Number.parseFloat(scenario.monthlyIncome || "0"), + monthlyExpenses: Number.parseFloat(scenario.monthlyExpenses || "0"), + targetAmount: scenario.targetAmount ? Number.parseFloat(scenario.targetAmount) : undefined, + horizonMonths: Number.parseInt(scenario.horizonMonths || "36", 10), + annualGrowthRate: Number.parseFloat(scenario.annualGrowthRate || "0"), + })); + if (!scenarios.length) { + setFutureStatus("Add at least one scenario."); + return; + } + + setFutureStatus("Calculating future scenarios..."); + const res = await apiFetch(`/api/households/${selectedId}/future-scenarios`, { + method: "POST", + body: JSON.stringify({ scenarios }), + }); + if (res.error) { + setFutureResult(null); + setFutureStatus(res.error.message ?? "Unable to calculate future scenarios."); + return; + } + setFutureResult(res.data ?? null); + setFutureStatus(""); + }; + const updateMemberAccess = async (member: HouseholdMember, payload: { role?: string; status?: string }) => { if (!selectedId) return; const res = await apiFetch(`/api/households/${selectedId}/members/${member.id}`, { @@ -1037,6 +1126,87 @@ export default function HouseholdSettingsPage() { +
+
+
+

Future planning scenarios

+

Compare what-if household plans

+

+ Project shared milestones, income changes, expense changes, or net worth plans over time. +

+ {futureResult ? ( +
+
+

Best final balance

+

{futureResult.comparison.bestFinalBalance.name}

+

{formatMoney(futureResult.comparison.bestFinalBalance.finalBalance)} after {futureResult.comparison.bestFinalBalance.horizonMonths} months

+
+ {futureResult.comparison.earliestTarget ? ( +
+

Earliest target

+

{futureResult.comparison.earliestTarget.name}

+

Month {futureResult.comparison.earliestTarget.targetReachedMonth}

+
+ ) : null} +
+ {futureResult.scenarios.map((scenario) => ( +
+
+
+

{scenario.name}

+

{scenario.recommendation}

+
+

{formatMoney(scenario.finalBalance)}

+
+
+ Net monthly: {formatMoney(scenario.monthlyNetChange)} + Growth: {formatMoney(scenario.projectedGrowth)} + Target: {scenario.targetReachedMonth ? `month ${scenario.targetReachedMonth}` : "not reached"} +
+
+ ))} +
+
+ ) : null} +
+
+
+ {futureDraft.scenarios.map((scenario, index) => ( +
+
+ updateFutureScenario(index, "name", event.target.value)} placeholder="Scenario name" className={inputClass} /> + + updateFutureScenario(index, "startingBalance", event.target.value)} placeholder="Starting balance" className={inputClass} /> + updateFutureScenario(index, "monthlyContribution", event.target.value)} placeholder="Monthly contribution" className={inputClass} /> + updateFutureScenario(index, "monthlyIncome", event.target.value)} placeholder="Monthly income change" className={inputClass} /> + updateFutureScenario(index, "monthlyExpenses", event.target.value)} placeholder="Monthly expense change" className={inputClass} /> + updateFutureScenario(index, "targetAmount", event.target.value)} placeholder="Target amount" className={inputClass} /> +
+ updateFutureScenario(index, "horizonMonths", event.target.value)} placeholder="Months" className={inputClass} /> + updateFutureScenario(index, "annualGrowthRate", event.target.value)} placeholder="Annual growth %" className={inputClass} /> +
+
+
+ ))} +
+
+ + +
+ {futureStatus ?

{futureStatus}

: null} +
+
+
+