Compare commits

..

No commits in common. "55b2bf61a5bf27ba6d20388d1f89fd6509942ecd" and "fdebc97c9a9d46685c1acbec514473ce7caf19b4" have entirely different histories.

3 changed files with 3 additions and 77 deletions

View file

@ -4,8 +4,6 @@ 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(), 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(), discretionaryExpense: z.string().nullish(), net: z.string(), months: z.array(MonthSchema).nullish(),
expenseCategories: z.array(NamedSchema).nullish(), incomePayees: z.array(NamedSchema).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 Named = z.infer<typeof NamedSchema>;
export type Analyze = z.infer<typeof AnalyzeSchema>; export type Analyze = z.infer<typeof AnalyzeSchema>;

View file

@ -25,7 +25,7 @@ import {
useSubscriptions, useSubscriptions,
useTransactions, useTransactions,
} from "../../api/hooks/reads"; } from "../../api/hooks/reads";
import type { Named, Analyze } from "../../api/schemas/analyze"; import type { Named } from "../../api/schemas/analyze";
import type { Subscription } from "../../api/schemas/subscription"; import type { Subscription } from "../../api/schemas/subscription";
import { buildHome, dec, type HomeData } from "./buildHome"; import { buildHome, dec, type HomeData } from "./buildHome";
import { buildTrendMonths, buildCashFlowVerdict, type TrendMonth } from "./cashFlowTrend"; import { buildTrendMonths, buildCashFlowVerdict, type TrendMonth } from "./cashFlowTrend";
@ -232,65 +232,6 @@ function Dot({ color }: { color: string }) {
return <span aria-hidden style={{ display: "inline-block", width: 7, height: 7, borderRadius: 2, background: color, marginRight: 5 }} />; 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 typical = dec(month?.monthlySalary);
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} />
{!loading && typical > 0 && (
<div style={{ fontSize: 12, color: "var(--seed-color-fg-neutral-muted, #8b8b8b)", marginTop: -6, marginLeft: 18 }}>
{s.incomeTypical} {hidden ? MASKED : tugrikShortRaw(typical)}
</div>
)}
<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 Нүүр (home) "money clarity" dashboard: a slim categorize-nudge banner,
* the safe-to-spend + cash-flow hero, where-your-money-went, recurring & * the safe-to-spend + cash-flow hero, where-your-money-went, recurring &
* subscriptions, and net worth + composition. Rebuilt per the mercury-rethink * subscriptions, and net worth + composition. Rebuilt per the mercury-rethink
@ -463,10 +404,7 @@ export function DashboardView() {
)} )}
</Link> </Link>
{/* 2. Income — salary vs support, and what was kept this month. */} {/* 2. Хаана зарцуулсан бэ? — top categories + top merchants. */}
<IncomeCard month={monthQ.data} loading={heroLoading} hidden={hiddenAmounts} />
{/* 3. Хаана зарцуулсан бэ? — top categories + top merchants. */}
<Card style={{ display: "flex", flexDirection: "column", gap: 16, padding: 20 }}> <Card style={{ display: "flex", flexDirection: "column", gap: 16, padding: 20 }}>
<SectionHeader title={s.whereWentTitle} /> <SectionHeader title={s.whereWentTitle} />
{whereItWentLoading ? ( {whereItWentLoading ? (

View file

@ -21,16 +21,6 @@ export const homeStrings = {
// Shared "see the full list" link, capped sections on the home cards. // Shared "see the full list" link, capped sections on the home cards.
viewAll: "Бүгдийг харах", viewAll: "Бүгдийг харах",
// Income card — earned salary vs support, and what's kept.
incomeTitle: "Энэ сарын орлого",
incomeSalary: "Цалин",
incomeTypical: "сарын дундаж",
incomeSupport: "Дэмжлэг ба бусад",
incomeSaved: "Хэмнэсэн",
incomeSpentOf: "зарцуулсан",
incomeSaveRate: (pct: number) => `Орлогынхоо ${pct}%-г үлдээсэн`,
incomeOverspent: "Энэ сар орлогоосоо илүү зарцуулсан",
// 2. Хаана зарцуулсан бэ? // 2. Хаана зарцуулсан бэ?
whereWentTitle: "Хаана зарцуулсан бэ?", whereWentTitle: "Хаана зарцуулсан бэ?",
categoriesLabel: "Ангилал", categoriesLabel: "Ангилал",