"use client"; import { useEffect, useState } from "react"; import { AppShell } from "../../components/app-shell"; import { apiFetch } from "@/lib/api"; type Household = { id: string; name: string }; type Budget = { id: string; name: string; category?: string | null; limitAmount: number; spentAmount: number; progressPercent: number; remainingAmount: number; status: string }; type Goal = { id: string; name: string; targetAmount: number; currentAmount: number; progressPercent: number; remainingAmount: number; status: string }; type InvestmentSummary = { holdings: Array<{ id: string; symbol: string; name: string; marketValue: number; quantity: number; price: number }>; totalMarketValue: number }; type NetWorthSummary = { computed: { assets: number; liabilities: number; netWorth: number; breakdown: Record }; history: Array<{ id: string; snapshotDate: string; netWorth: number }> }; type Recurring = { id: string; merchant: string; cadence: string; averageAmount: number; nextExpectedDate?: string | null; confidence: number }; export default function PlanningPage() { const [households, setHouseholds] = useState([]); const [householdId, setHouseholdId] = useState(""); const [budgets, setBudgets] = useState([]); const [goals, setGoals] = useState([]); const [investments, setInvestments] = useState({ holdings: [], totalMarketValue: 0 }); const [netWorth, setNetWorth] = useState(null); const [recurring, setRecurring] = useState([]); const [status, setStatus] = useState(""); const [budgetForm, setBudgetForm] = useState({ name: "", category: "", limitAmount: "500", spentAmount: "0" }); const [goalForm, setGoalForm] = useState({ name: "", targetAmount: "1000", currentAmount: "0" }); const [investmentForm, setInvestmentForm] = useState({ symbol: "", name: "", quantity: "1", price: "100" }); const load = async (selectedHousehold = householdId) => { const [householdRes, goalRes, investmentRes, netWorthRes, recurringRes] = await Promise.all([ apiFetch("/api/households"), apiFetch("/api/planning/goals"), apiFetch("/api/planning/investments"), apiFetch("/api/planning/net-worth"), apiFetch("/api/planning/recurring"), ]); if (!householdRes.error && householdRes.data) { setHouseholds(householdRes.data); if (!selectedHousehold && householdRes.data[0]?.id) { selectedHousehold = householdRes.data[0].id; setHouseholdId(selectedHousehold); } } if (!goalRes.error && goalRes.data) setGoals(goalRes.data); if (!investmentRes.error && investmentRes.data) setInvestments(investmentRes.data); if (!netWorthRes.error && netWorthRes.data) setNetWorth(netWorthRes.data); if (!recurringRes.error && recurringRes.data) setRecurring(recurringRes.data); if (selectedHousehold) { const budgetRes = await apiFetch(`/api/planning/budgets?householdId=${encodeURIComponent(selectedHousehold)}`); if (!budgetRes.error && budgetRes.data) setBudgets(budgetRes.data); } }; useEffect(() => { load(); }, []); const createBudget = async (event: React.FormEvent) => { event.preventDefault(); if (!householdId) return setStatus("Create or select a household first."); const res = await apiFetch("/api/planning/budgets", { method: "POST", body: JSON.stringify({ householdId, name: budgetForm.name, category: budgetForm.category || undefined, limitAmount: Number(budgetForm.limitAmount), spentAmount: Number(budgetForm.spentAmount), }), }); setStatus(res.error ? res.error.message ?? "Budget create failed." : "Budget created."); if (!res.error) { setBudgetForm({ name: "", category: "", limitAmount: "500", spentAmount: "0" }); load(householdId); } }; const createGoal = async (event: React.FormEvent) => { event.preventDefault(); const res = await apiFetch("/api/planning/goals", { method: "POST", body: JSON.stringify({ name: goalForm.name, targetAmount: Number(goalForm.targetAmount), currentAmount: Number(goalForm.currentAmount) }), }); setStatus(res.error ? res.error.message ?? "Goal create failed." : "Goal created."); if (!res.error) { setGoalForm({ name: "", targetAmount: "1000", currentAmount: "0" }); load(householdId); } }; const createInvestment = async (event: React.FormEvent) => { event.preventDefault(); const res = await apiFetch("/api/planning/investments", { method: "POST", body: JSON.stringify({ symbol: investmentForm.symbol, name: investmentForm.name, quantity: Number(investmentForm.quantity), price: Number(investmentForm.price), }), }); setStatus(res.error ? res.error.message ?? "Investment create failed." : "Investment added."); if (!res.error) { setInvestmentForm({ symbol: "", name: "", quantity: "1", price: "100" }); load(householdId); } }; const snapshotNetWorth = async () => { const res = await apiFetch("/api/planning/net-worth/snapshots", { method: "POST", body: JSON.stringify({}) }); setStatus(res.error ? res.error.message ?? "Snapshot failed." : "Net worth snapshot saved."); if (!res.error) load(householdId); }; const detectRecurring = async () => { const res = await apiFetch<{ detected: number }>("/api/planning/recurring/detect", { method: "POST", body: JSON.stringify({}) }); setStatus(res.error ? res.error.message ?? "Detection failed." : `Detected ${res.data?.detected ?? 0} recurring transactions.`); if (!res.error) load(householdId); }; const input = "rounded-lg border border-border bg-background/50 px-3 py-2 text-sm"; const card = "rounded-lg border border-border bg-background/40 p-4"; return (
{status ?
{status}
: null}
Net worth
${netWorth?.computed.netWorth?.toLocaleString() ?? "0"}
Investment value
${investments.totalMarketValue.toLocaleString()}
Recurring detected
{recurring.length}

Shared budgets

setBudgetForm({ ...budgetForm, name: e.target.value })} required /> setBudgetForm({ ...budgetForm, category: e.target.value })} /> setBudgetForm({ ...budgetForm, limitAmount: e.target.value })} /> setBudgetForm({ ...budgetForm, spentAmount: e.target.value })} />
{budgets.map((budget) => (
{budget.name}{budget.progressPercent}%
${Number(budget.spentAmount).toLocaleString()} of ${Number(budget.limitAmount).toLocaleString()} spent
))}

Personal goals

setGoalForm({ ...goalForm, name: e.target.value })} required /> setGoalForm({ ...goalForm, targetAmount: e.target.value })} /> setGoalForm({ ...goalForm, currentAmount: e.target.value })} />
{goals.map((goal) => (
{goal.name}{goal.progressPercent}%
${Number(goal.currentAmount).toLocaleString()} of ${Number(goal.targetAmount).toLocaleString()}
))}

Investments

setInvestmentForm({ ...investmentForm, symbol: e.target.value })} required /> setInvestmentForm({ ...investmentForm, name: e.target.value })} required /> setInvestmentForm({ ...investmentForm, quantity: e.target.value })} /> setInvestmentForm({ ...investmentForm, price: e.target.value })} />
{investments.holdings.map((holding) => (
{holding.symbol}${Number(holding.marketValue).toLocaleString()}
{holding.name}
))}

Recurring transactions

{recurring.map((item) => (
{item.merchant}{item.cadence}
${Number(item.averageAmount).toLocaleString()} average ยท {Math.round(Number(item.confidence) * 100)}% confidence
))} {!recurring.length ?
Run detection after importing transactions.
: null}
); }