feat(web): income card on home — salary vs support + what's kept
New card on the money-clarity home showing this month's income split into earned salary and support/other, with a spent-vs-income bar and the save rate. Surfaces the backend income breakdown so the reliable-earnings-vs- variable-support distinction is visible, not just in reports.
This commit is contained in:
parent
fdebc97c9a
commit
741d5ca0f4
3 changed files with 70 additions and 3 deletions
|
|
@ -4,6 +4,8 @@ export const MonthSchema = z.object({ month: z.string(), income: z.string(), exp
|
|||
export const AnalyzeSchema = z.object({ from: z.string(), to: z.string(), income: z.string(), expense: z.string(),
|
||||
discretionaryExpense: z.string().nullish(), net: z.string(), months: z.array(MonthSchema).nullish(),
|
||||
expenseCategories: z.array(NamedSchema).nullish(), incomePayees: z.array(NamedSchema).nullish(),
|
||||
expensePayees: z.array(NamedSchema).nullish() });
|
||||
expensePayees: z.array(NamedSchema).nullish(),
|
||||
salaryIncome: z.string().nullish(), otherIncome: z.string().nullish(),
|
||||
monthlySalary: z.string().nullish(), nextPayday: z.string().nullish() });
|
||||
export type Named = z.infer<typeof NamedSchema>;
|
||||
export type Analyze = z.infer<typeof AnalyzeSchema>;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
useSubscriptions,
|
||||
useTransactions,
|
||||
} from "../../api/hooks/reads";
|
||||
import type { Named } from "../../api/schemas/analyze";
|
||||
import type { Named, Analyze } from "../../api/schemas/analyze";
|
||||
import type { Subscription } from "../../api/schemas/subscription";
|
||||
import { buildHome, dec, type HomeData } from "./buildHome";
|
||||
import { buildTrendMonths, buildCashFlowVerdict, type TrendMonth } from "./cashFlowTrend";
|
||||
|
|
@ -232,6 +232,59 @@ function Dot({ color }: { color: string }) {
|
|||
return <span aria-hidden style={{ display: "inline-block", width: 7, height: 7, borderRadius: 2, background: color, marginRight: 5 }} />;
|
||||
}
|
||||
|
||||
/** One labelled income component (salary / support) with a leading colour dot. */
|
||||
function IncomeRow({ label, value, dotColor, loading, hidden }: { label: string; value: number; dotColor: string; loading: boolean; hidden: boolean }) {
|
||||
return (
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 14 }}>
|
||||
<span aria-hidden style={{ width: 10, height: 10, borderRadius: "50%", background: dotColor, flex: "none" }} />
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ fontSize: 15, fontWeight: 700 }}>
|
||||
<Amount value={value} loading={loading} hidden={hidden} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Income card: this month's earnings split into salary vs support/other, and
|
||||
* how much was kept, so reliable pay reads apart from variable family support. */
|
||||
function IncomeCard({ month, loading, hidden }: { month?: Analyze; loading: boolean; hidden: boolean }) {
|
||||
const salary = dec(month?.salaryIncome);
|
||||
const support = dec(month?.otherIncome);
|
||||
const income = dec(month?.income);
|
||||
const expense = dec(month?.expense);
|
||||
const saved = income - expense;
|
||||
const rate = income > 0 ? Math.round((saved / income) * 100) : 0;
|
||||
const spentFrac = income > 0 ? Math.min(1, expense / income) : 0;
|
||||
const over = saved < 0;
|
||||
const good = "var(--mercury-success, #1e9e6b)";
|
||||
const bad = "var(--mercury-critical, #e5484d)";
|
||||
return (
|
||||
<Card style={{ display: "flex", flexDirection: "column", gap: 16, padding: 20 }}>
|
||||
<SectionHeader title={s.incomeTitle} />
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<IncomeRow label={s.incomeSalary} value={salary} dotColor={good} loading={loading} hidden={hidden} />
|
||||
<IncomeRow label={s.incomeSupport} value={support} dotColor="var(--mercury-brand-yellow, #f2b021)" loading={loading} hidden={hidden} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ height: 8, borderRadius: 999, background: "var(--seed-color-bg-neutral-subtle, #eef0f2)", overflow: "hidden" }}>
|
||||
<div style={{ height: "100%", width: `${Math.max(2, spentFrac * 100)}%`, borderRadius: 999, background: over ? bad : good }} />
|
||||
</div>
|
||||
<div style={{ marginTop: 10, display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 8 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: over ? bad : good }}>
|
||||
{over ? s.incomeOverspent : s.incomeSaveRate(rate)}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: over ? bad : good, flex: "none" }}>
|
||||
{over ? "−" : "+"}
|
||||
<Amount value={Math.abs(saved)} loading={loading} hidden={hidden} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/** The Нүүр (home) "money clarity" dashboard: a slim categorize-nudge banner,
|
||||
* the safe-to-spend + cash-flow hero, where-your-money-went, recurring &
|
||||
* subscriptions, and net worth + composition. Rebuilt per the mercury-rethink
|
||||
|
|
@ -404,7 +457,10 @@ export function DashboardView() {
|
|||
)}
|
||||
</Link>
|
||||
|
||||
{/* 2. Хаана зарцуулсан бэ? — top categories + top merchants. */}
|
||||
{/* 2. Income — salary vs support, and what was kept this month. */}
|
||||
<IncomeCard month={monthQ.data} loading={heroLoading} hidden={hiddenAmounts} />
|
||||
|
||||
{/* 3. Хаана зарцуулсан бэ? — top categories + top merchants. */}
|
||||
<Card style={{ display: "flex", flexDirection: "column", gap: 16, padding: 20 }}>
|
||||
<SectionHeader title={s.whereWentTitle} />
|
||||
{whereItWentLoading ? (
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ export const homeStrings = {
|
|||
// Shared "see the full list" link, capped sections on the home cards.
|
||||
viewAll: "Бүгдийг харах",
|
||||
|
||||
// Income card — earned salary vs support, and what's kept.
|
||||
incomeTitle: "Энэ сарын орлого",
|
||||
incomeSalary: "Цалин",
|
||||
incomeSupport: "Дэмжлэг ба бусад",
|
||||
incomeSaved: "Хэмнэсэн",
|
||||
incomeSpentOf: "зарцуулсан",
|
||||
incomeSaveRate: (pct: number) => `Орлогынхоо ${pct}%-г үлдээсэн`,
|
||||
incomeOverspent: "Энэ сар орлогоосоо илүү зарцуулсан",
|
||||
|
||||
// 2. Хаана зарцуулсан бэ?
|
||||
whereWentTitle: "Хаана зарцуулсан бэ?",
|
||||
categoriesLabel: "Ангилал",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue