Add future planning scenarios UI

This commit is contained in:
MOHAN 2026-07-16 23:57:17 +05:30
parent f154fd5656
commit c1ff8a44b8
2 changed files with 179 additions and 0 deletions

View File

@ -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`);
}

View File

@ -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<DebtPayoffResult | null>(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<FutureScenarioResponse | null>(null);
const [futureStatus, setFutureStatus] = useState("");
const [privacyStatus, setPrivacyStatus] = useState("");
const [moneyDatePrompts, setMoneyDatePrompts] = useState<MoneyDatePromptResponse | null>(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<FutureScenarioResponse>(`/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<HouseholdMember>(`/api/households/${selectedId}/members/${member.id}`, {
@ -1037,6 +1126,87 @@ export default function HouseholdSettingsPage() {
</div>
</div>
<div className="glass-panel rounded-2xl p-6 shadow-sm">
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Future planning scenarios</p>
<h2 className="mt-2 text-xl font-bold text-foreground">Compare what-if household plans</h2>
<p className="mt-2 text-sm text-muted-foreground">
Project shared milestones, income changes, expense changes, or net worth plans over time.
</p>
{futureResult ? (
<div className="mt-5 space-y-3">
<div className="rounded-xl border border-border bg-background/40 p-4">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Best final balance</p>
<p className="mt-2 text-2xl font-bold text-foreground">{futureResult.comparison.bestFinalBalance.name}</p>
<p className="text-sm text-muted-foreground">{formatMoney(futureResult.comparison.bestFinalBalance.finalBalance)} after {futureResult.comparison.bestFinalBalance.horizonMonths} months</p>
</div>
{futureResult.comparison.earliestTarget ? (
<div className="rounded-xl border border-border bg-background/40 p-4">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Earliest target</p>
<p className="mt-2 text-2xl font-bold text-foreground">{futureResult.comparison.earliestTarget.name}</p>
<p className="text-sm text-muted-foreground">Month {futureResult.comparison.earliestTarget.targetReachedMonth}</p>
</div>
) : null}
<div className="space-y-3">
{futureResult.scenarios.map((scenario) => (
<div key={scenario.name} className="rounded-xl border border-border bg-background/40 p-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<p className="font-semibold text-foreground">{scenario.name}</p>
<p className="text-sm text-muted-foreground">{scenario.recommendation}</p>
</div>
<p className="text-lg font-bold text-foreground">{formatMoney(scenario.finalBalance)}</p>
</div>
<div className="mt-3 grid gap-2 text-xs text-muted-foreground sm:grid-cols-3">
<span>Net monthly: {formatMoney(scenario.monthlyNetChange)}</span>
<span>Growth: {formatMoney(scenario.projectedGrowth)}</span>
<span>Target: {scenario.targetReachedMonth ? `month ${scenario.targetReachedMonth}` : "not reached"}</span>
</div>
</div>
))}
</div>
</div>
) : null}
</div>
<form onSubmit={calculateFutureScenarios} className="space-y-4">
<div className="space-y-3">
{futureDraft.scenarios.map((scenario, index) => (
<div key={index} className="rounded-xl border border-border bg-background/40 p-3">
<div className="grid gap-2 md:grid-cols-2">
<input value={scenario.name} onChange={(event) => updateFutureScenario(index, "name", event.target.value)} placeholder="Scenario name" className={inputClass} />
<select value={scenario.type} onChange={(event) => updateFutureScenario(index, "type", event.target.value)} className={inputClass}>
<option value="goal">Goal</option>
<option value="net_worth">Net worth</option>
<option value="income_change">Income change</option>
<option value="expense_change">Expense change</option>
</select>
<input type="number" step="0.01" value={scenario.startingBalance} onChange={(event) => updateFutureScenario(index, "startingBalance", event.target.value)} placeholder="Starting balance" className={inputClass} />
<input type="number" step="0.01" value={scenario.monthlyContribution} onChange={(event) => updateFutureScenario(index, "monthlyContribution", event.target.value)} placeholder="Monthly contribution" className={inputClass} />
<input type="number" step="0.01" value={scenario.monthlyIncome} onChange={(event) => updateFutureScenario(index, "monthlyIncome", event.target.value)} placeholder="Monthly income change" className={inputClass} />
<input type="number" step="0.01" value={scenario.monthlyExpenses} onChange={(event) => updateFutureScenario(index, "monthlyExpenses", event.target.value)} placeholder="Monthly expense change" className={inputClass} />
<input type="number" min="0" step="0.01" value={scenario.targetAmount} onChange={(event) => updateFutureScenario(index, "targetAmount", event.target.value)} placeholder="Target amount" className={inputClass} />
<div className="grid gap-2 sm:grid-cols-2">
<input type="number" min="1" max="600" step="1" value={scenario.horizonMonths} onChange={(event) => updateFutureScenario(index, "horizonMonths", event.target.value)} placeholder="Months" className={inputClass} />
<input type="number" min="0" step="0.01" value={scenario.annualGrowthRate} onChange={(event) => updateFutureScenario(index, "annualGrowthRate", event.target.value)} placeholder="Annual growth %" className={inputClass} />
</div>
</div>
</div>
))}
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<button type="button" onClick={addFutureScenario} className="rounded-lg border border-border px-4 py-2 text-sm font-semibold text-foreground hover:bg-secondary">
Add scenario
</button>
<button type="submit" className="rounded-lg bg-primary px-4 py-2 text-sm font-bold text-primary-foreground hover:bg-primary/90">
Compare scenarios
</button>
</div>
{futureStatus ? <p className="text-sm text-muted-foreground">{futureStatus}</p> : null}
</form>
</div>
</div>
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
<div className="glass-panel rounded-2xl p-6 shadow-sm">
<div className="flex items-center justify-between gap-3">