diff --git a/src/features/home/DashboardView.tsx b/src/features/home/DashboardView.tsx
index d6c0683..46bc616 100644
--- a/src/features/home/DashboardView.tsx
+++ b/src/features/home/DashboardView.tsx
@@ -2,6 +2,7 @@
import * as React from "react";
import Link from "next/link";
+import { useRouter } from "next/navigation";
import { Skeleton } from "@seed-design/react";
import {
Card,
@@ -11,11 +12,11 @@ import {
Icon,
HideAmountsToggle,
SyncButton,
+ MercuryButton,
categoryStyle,
} from "../../ds";
import type { IconName } from "../../ds/icons";
-import { AmountToggle } from "../../ds/AmountToggle";
-import { tugrik, tugrikShort, tugrikShortRaw } from "../../ds/money";
+import { tugrik, tugrikRaw, tugrikShort, tugrikShortRaw, MASKED } from "../../ds/money";
import {
useNetWorth,
useAnalyzeMonth,
@@ -32,18 +33,54 @@ import { topExpenseCategories, topExpensePayees, shareOf } from "./whereItWent";
import { recurringMonthlyTotal, detectRecurringMerchants } from "./recurring";
import { buildNetWorthComposition } from "./netWorthComposition";
import { countUncategorized } from "./categorizeNudge";
+import { prettyMerchant } from "./prettyMerchant";
import { sample } from "./sample";
import { homeStrings as s } from "./strings";
import { monthRange } from "../accounting/monthRange";
import { useHiddenAmounts } from "../accounting/useHiddenAmounts";
-/** A number that's redacted with a Seed skeleton block while loading, and a
- * tap-to-reveal `AmountToggle` once real data has arrived. Mirrors iOS's
- * `.skeleton(loading)` view modifier, which redacts the finished layout in
- * place rather than swapping in a separate spinner. */
-function Amount({ value, loading, width = "72px" }: { value: number; loading: boolean; width?: string }) {
+/** A number that's redacted with a Seed skeleton block while loading. Once
+ * real data has arrived it renders the REAL figure by default — masked only
+ * when the global hide-amounts flag (`hidden`) is on — never the
+ * default-hidden tap-to-reveal `AmountToggle`, which is for the ledger rows,
+ * not the hero. Mirrors iOS's `.skeleton(loading)` view modifier, which
+ * redacts the finished layout in place rather than swapping in a spinner. */
+function Amount({
+ value,
+ loading,
+ hidden,
+ width = "72px",
+}: {
+ value: number;
+ loading: boolean;
+ hidden: boolean;
+ width?: string;
+}) {
if (loading) return ;
- return ;
+ return <>{hidden ? MASKED : tugrikRaw(value)}>;
+}
+
+/** The small "see the rest" link capping every home list — categories,
+ * merchants, recurring. Always routes to the full-detail page for that
+ * data, never a dead end. */
+function ViewAllLink({ href, label }: { href: string; label: string }) {
+ return (
+
+ {label}
+
+ );
}
/** The circular "₮" badge used on every card (togrogCircle in DashboardView.swift). */
@@ -76,15 +113,15 @@ function TugrikCircle({ bg, fg }: { bg: string; fg: string }) {
function CashFlowMiniChart({ months }: { months: TrendMonth[] }) {
const max = Math.max(1, ...months.flatMap((m) => [m.income, m.expense]));
const W = 300;
- const H = 78;
- const base = H - 14;
- const top = 6;
+ const H = 92;
+ const base = H - 16;
+ const top = 8;
const groupW = W / months.length;
const barW = Math.min(16, groupW / 3.2);
return (
-
@@ -203,6 +240,7 @@ function Dot({ color }: { color: string }) {
* unchanged) plus the small aggregation helpers in this directory; `sample`
* is the pre-connection / empty-account fallback for the hero only. */
export function DashboardView() {
+ const router = useRouter();
const netWorthQ = useNetWorth();
const monthQ = useAnalyzeMonth();
const budgetQ = useBudget();
@@ -213,8 +251,9 @@ export function DashboardView() {
// Subscribes this component to the global hide-amounts flag so every
// `tugrik()`/`tugrikShort()` call below (which read the flag internally,
- // but don't themselves trigger a re-render) reflects a live toggle.
- useHiddenAmounts();
+ // but don't themselves trigger a re-render) reflects a live toggle, and so
+ // the hero's own `` prop stays in sync with it.
+ const hiddenAmounts = useHiddenAmounts();
const heroLoading = netWorthQ.isLoading || monthQ.isLoading || budgetQ.isLoading;
@@ -246,6 +285,21 @@ export function DashboardView() {
);
const detected = detectRecurringMerchants(monthTxns, knownMatchKeys);
+ // Cap the recurring list at the top 5 by monthly amount (was dumping every
+ // subscription + bill unfiltered) — biggest commitments first, the rest is
+ // a tap away via the "Бүгдийг харах" link to the full manager.
+ const RECURRING_CAP = 5;
+ const recurringRows = React.useMemo(
+ () =>
+ [
+ ...subs.map((sub) => ({ sub, icon: "sparkles" as const })),
+ ...bills.map((sub) => ({ sub, icon: "card" as const })),
+ ].sort((a, b) => dec(b.sub.monthly) - dec(a.sub.monthly)),
+ [subs, bills],
+ );
+ const visibleRecurring = recurringRows.slice(0, RECURRING_CAP);
+ const hiddenRecurringCount = Math.max(0, recurringRows.length - RECURRING_CAP);
+
const composition = buildNetWorthComposition(netWorthQ.data);
const netWorthLoading = netWorthQ.isLoading;
const compTotal = composition.bankTotal + composition.manualTotal;
@@ -264,38 +318,37 @@ export function DashboardView() {
{/* 5. Categorize nudge — the memo's keystone: made visible at the top,
not tucked in as the last card. A slim banner, not a full module. */}
{!monthTxnsQ.isLoading && uncategorizedCount > 0 && (
-