feat: show household health score
This commit is contained in:
parent
59ea77ed44
commit
ab273ea7d0
@ -43,6 +43,26 @@ type CashflowMonth = {
|
||||
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 = {
|
||||
date: string;
|
||||
description: string;
|
||||
@ -88,6 +108,7 @@ type DashboardData = {
|
||||
monthlyNet: number;
|
||||
};
|
||||
ownershipBreakdown: Record<string, { accountCount: number; balance: number }>;
|
||||
healthScore: HealthScore;
|
||||
accounts: HouseholdAccount[];
|
||||
cashflow: CashflowMonth[];
|
||||
recentTransactions: RecentTransaction[];
|
||||
@ -116,6 +137,10 @@ function accountLabel(account: HouseholdAccount | RecentTransaction["account"])
|
||||
return `${account.institutionName ?? "Account"}${mask}`;
|
||||
}
|
||||
|
||||
function healthRatingLabel(value: string) {
|
||||
return value.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
export default function HouseholdSettingsPage() {
|
||||
const [households, setHouseholds] = useState<Household[]>([]);
|
||||
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>
|
||||
) : 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}>
|
||||
<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>
|
||||
@ -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-1 text-sm text-muted-foreground">Active household access</p>
|
||||
</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 className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user