From 741d5ca0f47257ac4bd122d3ef04f4b49f6b72d0 Mon Sep 17 00:00:00 2001 From: Munkherdene Date: Sun, 23 Aug 2026 18:19:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20income=20card=20on=20home=20?= =?UTF-8?q?=E2=80=94=20salary=20vs=20support=20+=20what's=20kept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/api/schemas/analyze.ts | 4 +- src/features/home/DashboardView.tsx | 60 ++++++++++++++++++++++++++++- src/features/home/strings.ts | 9 +++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/api/schemas/analyze.ts b/src/api/schemas/analyze.ts index fa8e39d..efe71cb 100644 --- a/src/api/schemas/analyze.ts +++ b/src/api/schemas/analyze.ts @@ -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; export type Analyze = z.infer; diff --git a/src/features/home/DashboardView.tsx b/src/features/home/DashboardView.tsx index 46bc616..60db322 100644 --- a/src/features/home/DashboardView.tsx +++ b/src/features/home/DashboardView.tsx @@ -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 ; } +/** 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 ( +
+ + + {label} + + + +
+ ); +} + +/** 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 ( + + +
+
+
+
+
+
+
+ + {over ? s.incomeOverspent : s.incomeSaveRate(rate)} + + + {over ? "−" : "+"} + +
+
+ + ); +} + /** 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() { )} - {/* 2. Хаана зарцуулсан бэ? — top categories + top merchants. */} + {/* 2. Income — salary vs support, and what was kept this month. */} +