"use client"; import { FormEvent, useEffect, useMemo, useState } from "react"; import { AppShell } from "../../../components/app-shell"; import { apiFetch } from "@/lib/api"; type Household = { id: string; name: string; members?: HouseholdMember[]; }; type HouseholdMember = { id: string; userId: string; role: string; joinedAt: string; user?: { email: string; fullName?: string | null; }; }; type HouseholdAccount = { displayId: string; institutionName: string; accountType: string; mask?: string | null; currentBalance: number; availableBalance: number; isoCurrencyCode: string; ownerUserId?: string | null; ownershipType: "mine" | "theirs" | "joint" | string; lastBalanceSync?: string | null; syncStatus: string; }; type CashflowMonth = { month: string; income: number; expenses: number; net: number; transactionCount: number; }; type RecentTransaction = { date: string; description: string; amount: number; source: string; category: string; account?: { institutionName?: string; mask?: string | null; ownerUserId?: string | null; ownershipType?: string; }; }; type DashboardData = { household: Household; members: HouseholdMember[]; summary: { memberCount: number; accountCount: number; totalBalance: number; availableBalance: number; monthlyIncome: number; monthlyExpenses: number; monthlyNet: number; }; ownershipBreakdown: Record; accounts: HouseholdAccount[]; cashflow: CashflowMonth[]; recentTransactions: RecentTransaction[]; }; const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }); const inputClass = "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"; const cardClass = "rounded-xl border border-border bg-secondary/10 p-5"; function formatMoney(value: number) { return money.format(value || 0); } function monthLabel(value: string) { const [year, month] = value.split("-").map(Number); return new Date(year, month - 1, 1).toLocaleDateString("en-US", { month: "short" }); } function accountLabel(account: HouseholdAccount | RecentTransaction["account"]) { if (!account) return "Household account"; const mask = account.mask ? ` ending ${account.mask}` : ""; return `${account.institutionName ?? "Account"}${mask}`; } export default function HouseholdSettingsPage() { const [households, setHouseholds] = useState([]); const [selectedId, setSelectedId] = useState(""); const [dashboard, setDashboard] = useState(null); const [newName, setNewName] = useState(""); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [status, setStatus] = useState(""); const selectedHousehold = useMemo( () => households.find((household) => household.id === selectedId) ?? null, [households, selectedId], ); useEffect(() => { loadHouseholds(); }, []); useEffect(() => { if (!selectedId) { setDashboard(null); return; } loadDashboard(selectedId); }, [selectedId]); const loadHouseholds = async () => { setLoading(true); const res = await apiFetch("/api/households"); setLoading(false); if (res.error) { setStatus(res.error.message ?? "Unable to load households."); return; } const list = res.data ?? []; setHouseholds(list); setSelectedId((current) => current || list[0]?.id || ""); }; const loadDashboard = async (id: string) => { setStatus("Loading shared dashboard..."); const res = await apiFetch(`/api/households/${id}/dashboard`); if (res.error) { setDashboard(null); setStatus(res.error.message ?? "Unable to load shared dashboard."); return; } setDashboard(res.data); setStatus(""); }; const createHousehold = async (event: FormEvent) => { event.preventDefault(); const name = newName.trim(); if (!name) return; setSaving(true); const res = await apiFetch("/api/households", { method: "POST", body: JSON.stringify({ name }), }); setSaving(false); if (res.error) { setStatus(res.error.message ?? "Unable to create household."); return; } setNewName(""); await loadHouseholds(); if (res.data?.id) setSelectedId(res.data.id); }; const maxCashflow = Math.max( 1, ...(dashboard?.cashflow ?? []).map((month) => Math.max(month.income, month.expenses)), ); return (

Active household

{selectedHousehold && (

{selectedHousehold.name} combines joint, mine, and partner-owned accounts without exposing stable account IDs in the page data.

)}
setNewName(event.target.value)} placeholder="Household name" className={`${inputClass} mt-2`} />
{status &&

{status}

}
{loading ? (
Loading households...
) : dashboard ? ( <>

Total balance

{formatMoney(dashboard.summary.totalBalance)}

{dashboard.summary.accountCount} shared accounts

Available

{formatMoney(dashboard.summary.availableBalance)}

Current liquid view

Monthly net

= 0 ? "text-green-500" : "text-red-500"}`}> {formatMoney(dashboard.summary.monthlyNet)}

{formatMoney(dashboard.summary.monthlyIncome)} in, {formatMoney(dashboard.summary.monthlyExpenses)} out

Members

{dashboard.summary.memberCount}

Active household access

Accounts by ownership

Mine / theirs / joint
{(["mine", "theirs", "joint"] as const).map((key) => { const item = dashboard.ownershipBreakdown[key] ?? { accountCount: 0, balance: 0 }; return (

{key}

{formatMoney(item.balance)}

{item.accountCount} accounts

); })}
{dashboard.accounts.map((account) => (

{accountLabel(account)}

{account.accountType} · {account.ownershipType} · {account.syncStatus}

{formatMoney(account.currentBalance)}

))} {!dashboard.accounts.length &&

No household accounts linked yet.

}

Members

{dashboard.members.map((member) => (

{member.user?.fullName || member.user?.email || member.userId}

{member.user?.email}

{member.role}
))}

Six-month cashflow

{dashboard.cashflow.map((month) => (
{monthLabel(month.month)} = 0 ? "text-green-500" : "text-red-500"}>{formatMoney(month.net)}
))}

Recent shared transactions

{dashboard.recentTransactions.map((transaction, index) => (

{transaction.description}

{new Date(transaction.date).toLocaleDateString()} · {transaction.category} · {accountLabel(transaction.account)}

{formatMoney(transaction.amount)}

))} {!dashboard.recentTransactions.length &&

No shared transactions yet.

}
) : (
Create a household to start viewing shared accounts and cashflow.
)}
); }