feat: show household health score
This commit is contained in:
parent
59ea77ed44
commit
ab273ea7d0
@ -43,6 +43,26 @@ type CashflowMonth = {
|
|||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HealthScore = {
|
||||||
|
score: number;
|
||||||
|
rating: "excellent" | "strong" | "building" | "needs_attention" | string;
|
||||||
|
components: {
|
||||||
|
cashflow: number;
|
||||||
|
balanceBuffer: number;
|
||||||
|
goalProgress: number;
|
||||||
|
collaboration: number;
|
||||||
|
syncHealth: number;
|
||||||
|
};
|
||||||
|
metrics: {
|
||||||
|
savingsRate: number;
|
||||||
|
bufferMonths: number;
|
||||||
|
activeGoalCount: number;
|
||||||
|
jointAccountCount: number;
|
||||||
|
collaborativeTransactionCount: number;
|
||||||
|
};
|
||||||
|
recommendations: string[];
|
||||||
|
};
|
||||||
|
|
||||||
type RecentTransaction = {
|
type RecentTransaction = {
|
||||||
date: string;
|
date: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -88,6 +108,7 @@ type DashboardData = {
|
|||||||
monthlyNet: number;
|
monthlyNet: number;
|
||||||
};
|
};
|
||||||
ownershipBreakdown: Record<string, { accountCount: number; balance: number }>;
|
ownershipBreakdown: Record<string, { accountCount: number; balance: number }>;
|
||||||
|
healthScore: HealthScore;
|
||||||
accounts: HouseholdAccount[];
|
accounts: HouseholdAccount[];
|
||||||
cashflow: CashflowMonth[];
|
cashflow: CashflowMonth[];
|
||||||
recentTransactions: RecentTransaction[];
|
recentTransactions: RecentTransaction[];
|
||||||
@ -116,6 +137,10 @@ function accountLabel(account: HouseholdAccount | RecentTransaction["account"])
|
|||||||
return `${account.institutionName ?? "Account"}${mask}`;
|
return `${account.institutionName ?? "Account"}${mask}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function healthRatingLabel(value: string) {
|
||||||
|
return value.replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
export default function HouseholdSettingsPage() {
|
export default function HouseholdSettingsPage() {
|
||||||
const [households, setHouseholds] = useState<Household[]>([]);
|
const [households, setHouseholds] = useState<Household[]>([]);
|
||||||
const [selectedId, setSelectedId] = useState("");
|
const [selectedId, setSelectedId] = useState("");
|
||||||
@ -343,7 +368,7 @@ export default function HouseholdSettingsPage() {
|
|||||||
<div className="glass-panel rounded-2xl p-8 text-sm text-muted-foreground">Loading households...</div>
|
<div className="glass-panel rounded-2xl p-8 text-sm text-muted-foreground">Loading households...</div>
|
||||||
) : dashboard ? (
|
) : dashboard ? (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||||
<div className={cardClass}>
|
<div className={cardClass}>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Total balance</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Total balance</p>
|
||||||
<p className="mt-2 text-3xl font-bold text-foreground">{formatMoney(dashboard.summary.totalBalance)}</p>
|
<p className="mt-2 text-3xl font-bold text-foreground">{formatMoney(dashboard.summary.totalBalance)}</p>
|
||||||
@ -368,6 +393,51 @@ export default function HouseholdSettingsPage() {
|
|||||||
<p className="mt-2 text-3xl font-bold text-foreground">{dashboard.summary.memberCount}</p>
|
<p className="mt-2 text-3xl font-bold text-foreground">{dashboard.summary.memberCount}</p>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">Active household access</p>
|
<p className="mt-1 text-sm text-muted-foreground">Active household access</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={cardClass}>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Health score</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-foreground">{dashboard.healthScore.score}</p>
|
||||||
|
<p className="mt-1 text-sm capitalize text-muted-foreground">{healthRatingLabel(dashboard.healthScore.rating)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass-panel rounded-2xl p-6 shadow-sm">
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[260px_1fr_1fr]">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Couples financial health</p>
|
||||||
|
<p className="mt-2 text-5xl font-bold text-foreground">{dashboard.healthScore.score}</p>
|
||||||
|
<p className="mt-1 text-sm capitalize text-muted-foreground">{healthRatingLabel(dashboard.healthScore.rating)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
{[
|
||||||
|
["Cashflow", dashboard.healthScore.components.cashflow, 35],
|
||||||
|
["Balance buffer", dashboard.healthScore.components.balanceBuffer, 20],
|
||||||
|
["Goal progress", dashboard.healthScore.components.goalProgress, 20],
|
||||||
|
["Collaboration", dashboard.healthScore.components.collaboration, 15],
|
||||||
|
["Sync health", dashboard.healthScore.components.syncHealth, 10],
|
||||||
|
].map(([label, value, max]) => (
|
||||||
|
<div key={label} className="rounded-xl border border-border bg-background/40 p-3">
|
||||||
|
<div className="flex items-center justify-between gap-3 text-sm">
|
||||||
|
<span className="font-semibold text-foreground">{label}</span>
|
||||||
|
<span className="text-muted-foreground">{value}/{max}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 h-2 overflow-hidden rounded-full bg-secondary">
|
||||||
|
<div className="h-full bg-primary" style={{ width: `${Math.min((Number(value) / Number(max)) * 100, 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-border bg-background/40 p-4">
|
||||||
|
<p className="text-sm font-semibold text-foreground">Next best moves</p>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{dashboard.healthScore.recommendations.map((recommendation) => (
|
||||||
|
<p key={recommendation} className="text-sm text-muted-foreground">{recommendation}</p>
|
||||||
|
))}
|
||||||
|
{!dashboard.healthScore.recommendations.length && (
|
||||||
|
<p className="text-sm text-muted-foreground">Shared cashflow, goals, collaboration, and sync health are all in strong shape.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
|
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user