merge rw/assets: visual rework to reference bar
This commit is contained in:
commit
15f2879f8b
6 changed files with 236 additions and 152 deletions
|
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useNetWorth, useTransactions } from "@/api/hooks/reads";
|
import { useNetWorth, useTransactions } from "@/api/hooks/reads";
|
||||||
import { Card } from "@/ds";
|
import { Card, IconChip, EmptyState } from "@/ds";
|
||||||
|
import { categoryStyle } from "@/ds/categoryStyle";
|
||||||
import { tugrikRaw } from "@/ds/money";
|
import { tugrikRaw } from "@/ds/money";
|
||||||
|
import type { Txn } from "@/api/schemas";
|
||||||
import { assetsStrings as s } from "./strings";
|
import { assetsStrings as s } from "./strings";
|
||||||
|
import { DetailHeader } from "./DetailHeader";
|
||||||
|
|
||||||
const mutedStyle: React.CSSProperties = { color: "var(--seed-color-fg-neutral-subtle)" };
|
const mutedStyle: React.CSSProperties = { color: "var(--seed-color-fg-neutral-subtle)" };
|
||||||
|
|
||||||
|
|
@ -26,10 +29,7 @@ export function AccountDetail({ accountId }: AccountDetailProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
||||||
<header style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
<DetailHeader title={account?.bank ?? s.accountDetail.bank} />
|
||||||
<BackLink />
|
|
||||||
<h1 style={{ margin: 0, fontSize: 18, fontWeight: 700, flex: 1 }}>{account?.bank ?? s.accountDetail.bank}</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<Card style={{ textAlign: "center", padding: "24px 20px" }}>
|
<Card style={{ textAlign: "center", padding: "24px 20px" }}>
|
||||||
<div style={{ fontSize: 12, ...mutedStyle }}>{account?.accountNumber}</div>
|
<div style={{ fontSize: 12, ...mutedStyle }}>{account?.accountNumber}</div>
|
||||||
|
|
@ -44,37 +44,22 @@ export function AccountDetail({ accountId }: AccountDetailProps) {
|
||||||
<DetailRow label={s.accountDetail.currency} value={account?.currency ?? "—"} />
|
<DetailRow label={s.accountDetail.currency} value={account?.currency ?? "—"} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<section>
|
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
<h3 style={{ margin: "0 0 10px", fontSize: 14, fontWeight: 700 }}>{s.accountDetail.recentTransactions}</h3>
|
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 700 }}>{s.accountDetail.recentTransactions}</h3>
|
||||||
{transactions.isLoading ? (
|
{transactions.isLoading ? (
|
||||||
<p style={{ fontSize: 13, ...mutedStyle }}>…</p>
|
<p style={{ fontSize: 13, ...mutedStyle }}>…</p>
|
||||||
) : (transactions.data ?? []).length === 0 ? (
|
) : (transactions.data ?? []).length === 0 ? (
|
||||||
<p style={{ fontSize: 13, ...mutedStyle }}>{s.accountDetail.noTransactions}</p>
|
<Card style={{ padding: 0 }}>
|
||||||
|
<EmptyState icon="receipt" title={s.accountDetail.noTransactions} compact />
|
||||||
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card style={{ padding: 0 }}>
|
<Card style={{ padding: 0 }}>
|
||||||
{(transactions.data ?? []).map((txn, i) => {
|
{(transactions.data ?? []).map((txn, i) => (
|
||||||
const income = txn.direction === "income";
|
<React.Fragment key={i}>
|
||||||
return (
|
{i > 0 && <Divider />}
|
||||||
<React.Fragment key={i}>
|
<TxnRow txn={txn} />
|
||||||
{i > 0 && <Divider />}
|
</React.Fragment>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", padding: "14px 16px" }}>
|
))}
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: 600 }}>{txn.title || txn.category}</div>
|
|
||||||
<div style={{ fontSize: 12, ...mutedStyle }}>{txn.date.slice(0, 10)}</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontWeight: 700,
|
|
||||||
color: income ? "var(--seed-color-fg-positive)" : "var(--seed-color-fg-critical)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{income ? "+" : "−"}
|
|
||||||
{tugrikRaw(txn.amount)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -82,11 +67,32 @@ export function AccountDetail({ accountId }: AccountDetailProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BackLink() {
|
function TxnRow({ txn }: { txn: Txn }) {
|
||||||
|
const income = txn.direction === "income";
|
||||||
|
const cat = categoryStyle(txn.category, income);
|
||||||
return (
|
return (
|
||||||
<a href="/assets" style={{ textDecoration: "none", color: "inherit", fontWeight: 600 }}>
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "14px 16px" }}>
|
||||||
← {s.common.back}
|
<div style={{ display: "flex", alignItems: "center", gap: 12, minWidth: 0 }}>
|
||||||
</a>
|
<IconChip icon={cat.icon} tint={cat.tint} fg={cat.fg} />
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||||||
|
<span style={{ fontSize: 15, fontWeight: 700, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||||
|
{txn.title || cat.name}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 12, ...mutedStyle }}>{txn.date.slice(0, 10)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 700,
|
||||||
|
flexShrink: 0,
|
||||||
|
color: income ? "var(--seed-color-fg-positive)" : "var(--seed-color-fg-critical)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{income ? "+" : "−"}
|
||||||
|
{tugrikRaw(txn.amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from "@seed-design/react";
|
} from "@seed-design/react";
|
||||||
import { useNetWorth, useManualAssets, useLending } from "@/api/hooks/reads";
|
import { useNetWorth, useManualAssets, useLending } from "@/api/hooks/reads";
|
||||||
import { useManualAssetMutations, useLendingMutations } from "@/api/hooks/mutations";
|
import { useManualAssetMutations, useLendingMutations } from "@/api/hooks/mutations";
|
||||||
import { Card, HideAmountsToggle, MercuryButton } from "@/ds";
|
import { Card, HideAmountsToggle, MercuryButton, IconChip, SectionHeader, EmptyState, Icon, type IconName } from "@/ds";
|
||||||
import { tugrik, tugrikRaw } from "@/ds/money";
|
import { tugrik, tugrikRaw } from "@/ds/money";
|
||||||
import type { Account, ManualAsset, Lending } from "@/api/schemas";
|
import type { Account, ManualAsset, Lending } from "@/api/schemas";
|
||||||
import { assetsStrings as s } from "./strings";
|
import { assetsStrings as s } from "./strings";
|
||||||
|
|
@ -32,6 +32,31 @@ const rowStyle: React.CSSProperties = {
|
||||||
gap: 12,
|
gap: 12,
|
||||||
padding: "14px 16px",
|
padding: "14px 16px",
|
||||||
};
|
};
|
||||||
|
const titleStyle: React.CSSProperties = {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 700,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
};
|
||||||
|
const subtitleStyle: React.CSSProperties = { fontSize: 12, ...mutedStyle };
|
||||||
|
const amountStyle: React.CSSProperties = { fontSize: 15, fontWeight: 700, flexShrink: 0 };
|
||||||
|
|
||||||
|
// Bank/lending/asset chip tints — soft tones matching the categoryStyle
|
||||||
|
// palette used elsewhere in Mercury (transactions, planner).
|
||||||
|
const BANK_TINT = { tint: "#DDEAF6", fg: "#215C9A" };
|
||||||
|
const LENDING_TINT = { tint: "#E3F0D8", fg: "#3F7A1E" };
|
||||||
|
|
||||||
|
function assetChip(category: string): { icon: IconName; tint: string; fg: string } {
|
||||||
|
switch (category) {
|
||||||
|
case "car":
|
||||||
|
return { icon: "car", tint: "#DDE6F6", fg: "#2A4B9A" };
|
||||||
|
case "electronics":
|
||||||
|
return { icon: "monitor", tint: "#E1E5EA", fg: "#42505F" };
|
||||||
|
default:
|
||||||
|
return { icon: "layers", tint: "#EDEBE7", fg: "#6B6257" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function AssetsView() {
|
export function AssetsView() {
|
||||||
// Re-renders when the global hide-amounts flag flips (tugrik()/tugrikRaw()
|
// Re-renders when the global hide-amounts flag flips (tugrik()/tugrikRaw()
|
||||||
|
|
@ -65,11 +90,13 @@ export function AssetsView() {
|
||||||
<NetWorthCard netWorth={netWorth.data} loading={netWorth.isLoading} />
|
<NetWorthCard netWorth={netWorth.data} loading={netWorth.isLoading} />
|
||||||
|
|
||||||
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
<SectionTitle>{s.accounts.title}</SectionTitle>
|
<SectionHeader title={s.accounts.title} />
|
||||||
{netWorth.isLoading ? (
|
{netWorth.isLoading ? (
|
||||||
<SkeletonRows count={2} />
|
<SkeletonRows count={2} />
|
||||||
) : accounts.length === 0 ? (
|
) : accounts.length === 0 ? (
|
||||||
<EmptyText>{s.accounts.empty}</EmptyText>
|
<Card style={{ padding: 0 }}>
|
||||||
|
<EmptyState icon="bank" title={s.accounts.empty} hint={s.accounts.emptyHint} />
|
||||||
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card style={{ padding: 0 }}>
|
<Card style={{ padding: 0 }}>
|
||||||
{accounts.map((account, i) => (
|
{accounts.map((account, i) => (
|
||||||
|
|
@ -79,11 +106,14 @@ export function AssetsView() {
|
||||||
href={`/assets/account/${account.accountId}`}
|
href={`/assets/account/${account.accountId}`}
|
||||||
style={{ ...rowStyle, color: "inherit", textDecoration: "none" }}
|
style={{ ...rowStyle, color: "inherit", textDecoration: "none" }}
|
||||||
>
|
>
|
||||||
<div>
|
<div style={{ display: "flex", alignItems: "center", gap: 12, minWidth: 0 }}>
|
||||||
<div style={{ fontWeight: 600 }}>{account.bank}</div>
|
<IconChip icon="bank" {...BANK_TINT} />
|
||||||
<div style={{ fontSize: 12, ...mutedStyle }}>{account.accountNumber}</div>
|
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||||||
|
<span style={titleStyle}>{account.bank}</span>
|
||||||
|
<span style={subtitleStyle}>{account.accountNumber}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontWeight: 700 }}>{tugrik(account.balance)}</div>
|
<span style={amountStyle}>{tugrik(account.balance)}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
@ -92,11 +122,13 @@ export function AssetsView() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
<SectionHeaderRow title={s.manualAssets.title} onAdd={() => setAddAssetOpen(true)} addLabel={s.manualAssets.add} />
|
<SectionHeader title={s.manualAssets.title} onAction={() => setAddAssetOpen(true)} actionLabel={s.manualAssets.add} />
|
||||||
{manualAssets.isLoading ? (
|
{manualAssets.isLoading ? (
|
||||||
<SkeletonRows count={3} />
|
<SkeletonRows count={3} />
|
||||||
) : assets.length === 0 ? (
|
) : assets.length === 0 ? (
|
||||||
<EmptyBlock title={s.manualAssets.emptyTitle} subtitle={s.manualAssets.emptySubtitle} />
|
<Card style={{ padding: 0 }}>
|
||||||
|
<EmptyState icon="layers" title={s.manualAssets.emptyTitle} hint={s.manualAssets.emptySubtitle} />
|
||||||
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
{assets.map((asset) => (
|
{assets.map((asset) => (
|
||||||
|
|
@ -113,11 +145,13 @@ export function AssetsView() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
<SectionHeaderRow title={s.lending.title} onAdd={() => setAddLoanOpen(true)} addLabel={s.lending.add} />
|
<SectionHeader title={s.lending.title} onAction={() => setAddLoanOpen(true)} actionLabel={s.lending.add} />
|
||||||
{lending.isLoading ? (
|
{lending.isLoading ? (
|
||||||
<SkeletonRows count={2} />
|
<SkeletonRows count={2} />
|
||||||
) : loans.length === 0 ? (
|
) : loans.length === 0 ? (
|
||||||
<EmptyText>{s.lending.empty}</EmptyText>
|
<Card style={{ padding: 0 }}>
|
||||||
|
<EmptyState icon="hand-coins" title={s.lending.empty} hint={s.lending.emptyHint} />
|
||||||
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card style={{ padding: 0 }}>
|
<Card style={{ padding: 0 }}>
|
||||||
{loans.map((loan, i) => (
|
{loans.map((loan, i) => (
|
||||||
|
|
@ -201,21 +235,21 @@ export function AssetsView() {
|
||||||
|
|
||||||
function NetWorthCard({ netWorth, loading }: { netWorth?: { total: string; assets: string; liabilities: string }; loading: boolean }) {
|
function NetWorthCard({ netWorth, loading }: { netWorth?: { total: string; assets: string; liabilities: string }; loading: boolean }) {
|
||||||
return (
|
return (
|
||||||
<Card style={{ padding: "24px 20px", background: "var(--mercury-balance-card)" }}>
|
<Card style={{ padding: "26px 22px", background: "var(--mercury-balance-card)" }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Skeleton height="40px" />
|
<Skeleton height="40px" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div style={{ fontSize: 13, opacity: 0.75 }}>{s.netWorth.total}</div>
|
<div style={{ fontSize: 13, fontWeight: 600, letterSpacing: 0.2, opacity: 0.75 }}>{s.netWorth.total}</div>
|
||||||
<div style={{ fontSize: 32, fontWeight: 700, marginTop: 6 }}>{tugrik(netWorth?.total ?? "0")}</div>
|
<div style={{ fontSize: 34, fontWeight: 800, marginTop: 8, letterSpacing: -0.5 }}>{tugrik(netWorth?.total ?? "0")}</div>
|
||||||
<div style={{ display: "flex", gap: 20, marginTop: 16 }}>
|
<div style={{ display: "flex", gap: 28, marginTop: 20 }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, opacity: 0.75 }}>{s.netWorth.assets}</div>
|
<div style={{ fontSize: 12, opacity: 0.75 }}>{s.netWorth.assets}</div>
|
||||||
<div style={{ fontWeight: 600 }}>{tugrik(netWorth?.assets ?? "0")}</div>
|
<div style={{ fontSize: 15, fontWeight: 700, marginTop: 2 }}>{tugrik(netWorth?.assets ?? "0")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, opacity: 0.75 }}>{s.netWorth.liabilities}</div>
|
<div style={{ fontSize: 12, opacity: 0.75 }}>{s.netWorth.liabilities}</div>
|
||||||
<div style={{ fontWeight: 600 }}>{tugrik(netWorth?.liabilities ?? "0")}</div>
|
<div style={{ fontSize: 15, fontWeight: 700, marginTop: 2 }}>{tugrik(netWorth?.liabilities ?? "0")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -226,48 +260,13 @@ function NetWorthCard({ netWorth, loading }: { netWorth?: { total: string; asset
|
||||||
|
|
||||||
// --- Small shared bits -------------------------------------------------------
|
// --- Small shared bits -------------------------------------------------------
|
||||||
|
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
|
||||||
return <h2 style={{ margin: 0, fontSize: 14, fontWeight: 700 }}>{children}</h2>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SectionHeaderRow({ title, onAdd, addLabel }: { title: string; onAdd: () => void; addLabel: string }) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
||||||
<SectionTitle>{title}</SectionTitle>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onAdd}
|
|
||||||
aria-label={addLabel}
|
|
||||||
style={{ background: "none", border: "none", cursor: "pointer", fontSize: 22, lineHeight: 1, padding: 4 }}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyText({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<p style={{ ...mutedStyle, fontSize: 13, padding: "12px 4px", margin: 0 }}>{children}</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyBlock({ title, subtitle }: { title: string; subtitle: string }) {
|
|
||||||
return (
|
|
||||||
<div style={{ textAlign: "center", padding: "40px 20px", display: "flex", flexDirection: "column", gap: 8 }}>
|
|
||||||
<div style={{ fontWeight: 600 }}>{title}</div>
|
|
||||||
<div style={{ fontSize: 13, ...mutedStyle }}>{subtitle}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SkeletonRows({ count }: { count: number }) {
|
function SkeletonRows({ count }: { count: number }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
<Card style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
{Array.from({ length: count }).map((_, i) => (
|
{Array.from({ length: count }).map((_, i) => (
|
||||||
<Skeleton key={i} height="64px" />
|
<Skeleton key={i} height="48px" style={{ borderRadius: "var(--seed-radius-r2, 8px)" }} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,25 +291,30 @@ function ManualAssetRow({
|
||||||
const positive = change >= 0;
|
const positive = change >= 0;
|
||||||
const categoryLabel = s.manualAssets.categories[asset.category] ?? asset.category;
|
const categoryLabel = s.manualAssets.categories[asset.category] ?? asset.category;
|
||||||
const conditionLabel = s.manualAssets.conditions[asset.condition] ?? asset.condition;
|
const conditionLabel = s.manualAssets.conditions[asset.condition] ?? asset.condition;
|
||||||
|
const chip = assetChip(asset.category);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card style={{ padding: 0 }}>
|
<Card style={{ padding: 0 }}>
|
||||||
<div style={rowStyle}>
|
<div style={rowStyle}>
|
||||||
<Link href={`/assets/manual/${encodeURIComponent(asset.name)}`} style={{ color: "inherit", textDecoration: "none", flex: 1 }}>
|
<Link
|
||||||
<div style={{ fontWeight: 700 }}>{asset.name}</div>
|
href={`/assets/manual/${encodeURIComponent(asset.name)}`}
|
||||||
<div style={{ fontSize: 12, ...mutedStyle }}>
|
style={{ display: "flex", alignItems: "center", gap: 12, color: "inherit", textDecoration: "none", flex: 1, minWidth: 0 }}
|
||||||
{categoryLabel} · {conditionLabel}
|
>
|
||||||
|
<IconChip icon={chip.icon} tint={chip.tint} fg={chip.fg} />
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||||||
|
<span style={titleStyle}>{asset.name}</span>
|
||||||
|
<span style={subtitleStyle}>
|
||||||
|
{categoryLabel} · {conditionLabel}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div style={{ textAlign: "right" }}>
|
<div style={{ textAlign: "right", flexShrink: 0 }}>
|
||||||
<div style={{ fontWeight: 700 }}>{tugrikRaw(asset.value)}</div>
|
<div style={amountStyle}>{tugrikRaw(asset.value)}</div>
|
||||||
<div style={{ fontSize: 11, ...mutedStyle }}>
|
|
||||||
{s.manualAssets.acquiredPrefix}: {tugrikRaw(asset.acquiredValue)}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
marginTop: 2,
|
||||||
color: positive ? "var(--seed-color-fg-positive)" : "var(--seed-color-fg-critical)",
|
color: positive ? "var(--seed-color-fg-positive)" : "var(--seed-color-fg-critical)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -319,32 +323,40 @@ function ManualAssetRow({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: 8, padding: "0 16px 12px" }}>
|
<div style={{ display: "flex", gap: 8, padding: "0 16px 14px" }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onRevalue}
|
onClick={onRevalue}
|
||||||
disabled={revaluing}
|
disabled={revaluing}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
border: "1px solid var(--seed-color-border-neutral, #e5e5e5)",
|
display: "flex",
|
||||||
borderRadius: 8,
|
alignItems: "center",
|
||||||
background: "none",
|
justifyContent: "center",
|
||||||
padding: "6px 10px",
|
gap: 6,
|
||||||
|
border: "none",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "var(--seed-color-bg-neutral-subtle, #eef0f2)",
|
||||||
|
padding: "8px 10px",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--seed-color-fg-neutral)",
|
||||||
cursor: revaluing ? "default" : "pointer",
|
cursor: revaluing ? "default" : "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Icon name="trending" size={14} />
|
||||||
{revaluing ? "…" : s.manualAssets.revalue}
|
{revaluing ? "…" : s.manualAssets.revalue}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
style={{
|
style={{
|
||||||
border: "1px solid var(--seed-color-border-neutral, #e5e5e5)",
|
border: "none",
|
||||||
borderRadius: 8,
|
borderRadius: 10,
|
||||||
background: "none",
|
background: "var(--seed-color-bg-neutral-subtle, #eef0f2)",
|
||||||
padding: "6px 10px",
|
padding: "8px 14px",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
color: "var(--seed-color-fg-critical)",
|
color: "var(--seed-color-fg-critical)",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
|
|
@ -373,21 +385,36 @@ function statusLabel(loan: Lending): string {
|
||||||
function LoanRow({ loan, onDelete }: { loan: Lending; onDelete: () => void }) {
|
function LoanRow({ loan, onDelete }: { loan: Lending; onDelete: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div style={rowStyle}>
|
<div style={rowStyle}>
|
||||||
<Link href={`/assets/lending/${loan.id}`} style={{ color: "inherit", textDecoration: "none", flex: 1 }}>
|
<Link
|
||||||
<div style={{ fontWeight: 600 }}>{loan.person}</div>
|
href={`/assets/lending/${loan.id}`}
|
||||||
<div style={{ fontSize: 12, color: loan.overdue ? "var(--seed-color-fg-critical)" : "var(--seed-color-fg-neutral-subtle)" }}>
|
style={{ display: "flex", alignItems: "center", gap: 12, color: "inherit", textDecoration: "none", flex: 1, minWidth: 0 }}
|
||||||
{statusLabel(loan)}
|
>
|
||||||
|
<IconChip icon="hand-coins" {...LENDING_TINT} />
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||||||
|
<span style={titleStyle}>{loan.person}</span>
|
||||||
|
<span style={{ fontSize: 12, color: loan.overdue ? "var(--seed-color-fg-critical)" : "var(--seed-color-fg-neutral-subtle)" }}>
|
||||||
|
{statusLabel(loan)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div style={{ textAlign: "right" }}>
|
<div style={{ textAlign: "right", flexShrink: 0 }}>
|
||||||
<div style={{ fontWeight: 700 }}>{tugrikRaw(loan.remaining)}</div>
|
<div style={amountStyle}>{tugrikRaw(loan.remaining)}</div>
|
||||||
<div style={{ fontSize: 12, ...mutedStyle }}>/ {tugrikRaw(loan.principal)}</div>
|
<div style={subtitleStyle}>/ {tugrikRaw(loan.principal)}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
aria-label={s.lending.delete}
|
aria-label={s.lending.delete}
|
||||||
style={{ background: "none", border: "none", color: "var(--seed-color-fg-neutral-subtle)", cursor: "pointer", fontSize: 16 }}
|
style={{
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
color: "var(--seed-color-fg-neutral-subtle)",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: 18,
|
||||||
|
lineHeight: 1,
|
||||||
|
padding: 4,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
47
src/features/assets/DetailHeader.tsx
Normal file
47
src/features/assets/DetailHeader.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Icon } from "@/ds";
|
||||||
|
import { assetsStrings as s } from "./strings";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared header for the account / manual-asset / lending detail pages: a
|
||||||
|
* round back button + title, matching the row/typography language used
|
||||||
|
* across the rest of the Assets feature.
|
||||||
|
*/
|
||||||
|
export function DetailHeader({ title }: { title: string }) {
|
||||||
|
return (
|
||||||
|
<header style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||||
|
<Link
|
||||||
|
href="/assets"
|
||||||
|
aria-label={s.common.back}
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
placeItems: "center",
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: 12,
|
||||||
|
background: "var(--seed-color-bg-neutral-subtle, #eef0f2)",
|
||||||
|
color: "var(--seed-color-fg-neutral)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="chevron-left" size={20} />
|
||||||
|
</Link>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 700,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -18,10 +18,11 @@ import {
|
||||||
import { useLending } from "@/api/hooks/reads";
|
import { useLending } from "@/api/hooks/reads";
|
||||||
import { useLendingMutations } from "@/api/hooks/mutations";
|
import { useLendingMutations } from "@/api/hooks/mutations";
|
||||||
import type { Lending } from "@/api/schemas";
|
import type { Lending } from "@/api/schemas";
|
||||||
import { Card, MercuryButton } from "@/ds";
|
import { Card, MercuryButton, IconChip, EmptyState } from "@/ds";
|
||||||
import { tugrikRaw } from "@/ds/money";
|
import { tugrikRaw } from "@/ds/money";
|
||||||
import { assetsStrings as s } from "./strings";
|
import { assetsStrings as s } from "./strings";
|
||||||
import { ConfirmDialog } from "./ConfirmDialog";
|
import { ConfirmDialog } from "./ConfirmDialog";
|
||||||
|
import { DetailHeader } from "./DetailHeader";
|
||||||
|
|
||||||
type LendingRepayment = Lending["repayments"][number];
|
type LendingRepayment = Lending["repayments"][number];
|
||||||
|
|
||||||
|
|
@ -70,7 +71,7 @@ export function LendingDetail({ id }: LendingDetailProps) {
|
||||||
if (!loan) {
|
if (!loan) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
<BackLink />
|
<DetailHeader title={s.lending.title} />
|
||||||
<p style={mutedStyle}>{s.lending.empty}</p>
|
<p style={mutedStyle}>{s.lending.empty}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -84,12 +85,12 @@ export function LendingDetail({ id }: LendingDetailProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
||||||
<header style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
<DetailHeader title={loan.person} />
|
||||||
<BackLink />
|
|
||||||
<h1 style={{ margin: 0, fontSize: 18, fontWeight: 700, flex: 1 }}>{loan.person}</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<Card style={{ textAlign: "center", padding: "24px 20px" }}>
|
<Card style={{ textAlign: "center", padding: "24px 20px" }}>
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", marginBottom: 12 }}>
|
||||||
|
<IconChip icon="hand-coins" tint="#E3F0D8" fg="#3F7A1E" size={48} />
|
||||||
|
</div>
|
||||||
<div style={{ fontSize: 32, fontWeight: 700 }}>{tugrikRaw(loan.remaining)}</div>
|
<div style={{ fontSize: 32, fontWeight: 700 }}>{tugrikRaw(loan.remaining)}</div>
|
||||||
<div style={{ marginTop: 6, ...mutedStyle }}>
|
<div style={{ marginTop: 6, ...mutedStyle }}>
|
||||||
{s.lending.total} <strong>{tugrikRaw(loan.principal)}</strong>
|
{s.lending.total} <strong>{tugrikRaw(loan.principal)}</strong>
|
||||||
|
|
@ -116,25 +117,28 @@ export function LendingDetail({ id }: LendingDetailProps) {
|
||||||
<strong style={{ color: "var(--seed-color-fg-positive)" }}>{tugrikRaw(loan.repaid)}</strong>
|
<strong style={{ color: "var(--seed-color-fg-positive)" }}>{tugrikRaw(loan.repaid)}</strong>
|
||||||
</div>
|
</div>
|
||||||
{loan.repayments.length === 0 ? (
|
{loan.repayments.length === 0 ? (
|
||||||
<p style={{ fontSize: 12, ...mutedStyle, margin: 0 }}>{s.lending.repayments.empty}</p>
|
<EmptyState icon="receipt" title={s.lending.repayments.empty} compact />
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||||
{loan.repayments.map((r, i) => (
|
{loan.repayments.map((r, i) => (
|
||||||
<React.Fragment key={r.id}>
|
<React.Fragment key={r.id}>
|
||||||
{i > 0 && <div style={{ height: 1, background: "var(--seed-color-border-neutral, #e5e5e5)" }} />}
|
{i > 0 && <div style={{ height: 1, background: "var(--seed-color-border-neutral, #e5e5e5)" }} />}
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 0" }}>
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "10px 0" }}>
|
||||||
<div>
|
<div style={{ display: "flex", alignItems: "center", gap: 12, minWidth: 0 }}>
|
||||||
<div style={{ fontWeight: 600 }}>{tugrikRaw(r.amount)}</div>
|
<IconChip icon="receipt" tint="#E1E5EA" fg="#42505F" size={36} />
|
||||||
<div style={{ fontSize: 12, ...mutedStyle }}>
|
<div style={{ minWidth: 0 }}>
|
||||||
{r.paidOn}
|
<div style={{ fontSize: 15, fontWeight: 700 }}>{tugrikRaw(r.amount)}</div>
|
||||||
{r.note ? ` · ${r.note}` : ""}
|
<div style={{ fontSize: 12, ...mutedStyle }}>
|
||||||
|
{r.paidOn}
|
||||||
|
{r.note ? ` · ${r.note}` : ""}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDeletingRepayment(r)}
|
onClick={() => setDeletingRepayment(r)}
|
||||||
aria-label={s.lending.repayments.deleteTitle}
|
aria-label={s.lending.repayments.deleteTitle}
|
||||||
style={{ background: "none", border: "none", cursor: "pointer", fontSize: 16, color: "var(--seed-color-fg-neutral-subtle)" }}
|
style={{ background: "none", border: "none", cursor: "pointer", fontSize: 18, flexShrink: 0, color: "var(--seed-color-fg-neutral-subtle)" }}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -190,14 +194,6 @@ export function LendingDetail({ id }: LendingDetailProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BackLink() {
|
|
||||||
return (
|
|
||||||
<a href="/assets" style={{ textDecoration: "none", color: "inherit", fontWeight: 600 }}>
|
|
||||||
← {s.common.back}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddRepaymentSheet({
|
function AddRepaymentSheet({
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,23 @@ import { apiGet } from "@/api/client";
|
||||||
import { useManualAssets } from "@/api/hooks/reads";
|
import { useManualAssets } from "@/api/hooks/reads";
|
||||||
import { useManualAssetMutations } from "@/api/hooks/mutations";
|
import { useManualAssetMutations } from "@/api/hooks/mutations";
|
||||||
import { AssetPointSchema, AssetListingSchema, type ManualAsset, type RevalueResult } from "@/api/schemas";
|
import { AssetPointSchema, AssetListingSchema, type ManualAsset, type RevalueResult } from "@/api/schemas";
|
||||||
import { Card, MercuryButton } from "@/ds";
|
import { Card, MercuryButton, IconChip, type IconName } from "@/ds";
|
||||||
import { tugrikRaw, tugrikShortRaw } from "@/ds/money";
|
import { tugrikRaw, tugrikShortRaw } from "@/ds/money";
|
||||||
import { assetsStrings as s } from "./strings";
|
import { assetsStrings as s } from "./strings";
|
||||||
import { ValueHistoryChart } from "./ValueHistoryChart";
|
import { ValueHistoryChart } from "./ValueHistoryChart";
|
||||||
import { ConfirmDialog } from "./ConfirmDialog";
|
import { ConfirmDialog } from "./ConfirmDialog";
|
||||||
|
import { DetailHeader } from "./DetailHeader";
|
||||||
|
|
||||||
|
function assetChip(category: string): { icon: IconName; tint: string; fg: string } {
|
||||||
|
switch (category) {
|
||||||
|
case "car":
|
||||||
|
return { icon: "car", tint: "#DDE6F6", fg: "#2A4B9A" };
|
||||||
|
case "electronics":
|
||||||
|
return { icon: "monitor", tint: "#E1E5EA", fg: "#42505F" };
|
||||||
|
default:
|
||||||
|
return { icon: "layers", tint: "#EDEBE7", fg: "#6B6257" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Local reads (no hook exists yet for asset history/listings; these mirror
|
// --- Local reads (no hook exists yet for asset history/listings; these mirror
|
||||||
// the shape of reads.ts's other hooks and hit the same endpoints iOS uses:
|
// the shape of reads.ts's other hooks and hit the same endpoints iOS uses:
|
||||||
|
|
@ -83,7 +95,7 @@ export function ManualAssetDetail({ name }: ManualAssetDetailProps) {
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
<BackLink />
|
<DetailHeader title={name} />
|
||||||
<p style={mutedStyle}>{name}</p>
|
<p style={mutedStyle}>{name}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -109,14 +121,16 @@ export function ManualAssetDetail({ name }: ManualAssetDetailProps) {
|
||||||
const categoryLabel = s.manualAssets.categories[asset.category] ?? asset.category;
|
const categoryLabel = s.manualAssets.categories[asset.category] ?? asset.category;
|
||||||
const conditionLabel = s.manualAssets.conditions[asset.condition] ?? asset.condition;
|
const conditionLabel = s.manualAssets.conditions[asset.condition] ?? asset.condition;
|
||||||
|
|
||||||
|
const chip = assetChip(asset.category);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
||||||
<header style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
<DetailHeader title={asset.name} />
|
||||||
<BackLink />
|
|
||||||
<h1 style={{ margin: 0, fontSize: 18, fontWeight: 700, flex: 1 }}>{asset.name}</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<Card style={{ textAlign: "center", padding: "24px 20px" }}>
|
<Card style={{ textAlign: "center", padding: "24px 20px" }}>
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", marginBottom: 12 }}>
|
||||||
|
<IconChip icon={chip.icon} tint={chip.tint} fg={chip.fg} size={48} />
|
||||||
|
</div>
|
||||||
<div style={{ fontSize: 12, ...mutedStyle }}>{categoryLabel} · {conditionLabel}</div>
|
<div style={{ fontSize: 12, ...mutedStyle }}>{categoryLabel} · {conditionLabel}</div>
|
||||||
<div style={{ fontSize: 32, fontWeight: 700, marginTop: 8 }}>{tugrikRaw(value)}</div>
|
<div style={{ fontSize: 32, fontWeight: 700, marginTop: 8 }}>{tugrikRaw(value)}</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -240,14 +254,6 @@ export function ManualAssetDetail({ name }: ManualAssetDetailProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BackLink() {
|
|
||||||
return (
|
|
||||||
<a href="/assets" style={{ textDecoration: "none", color: "inherit", fontWeight: 600 }}>
|
|
||||||
← {s.common.back}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DetailRow({ label, value, color }: { label: string; value: string; color?: string }) {
|
function DetailRow({ label, value, color }: { label: string; value: string; color?: string }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", padding: "14px 0" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", padding: "14px 0" }}>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export const assetsStrings = {
|
||||||
accounts: {
|
accounts: {
|
||||||
title: "Миний дансууд",
|
title: "Миний дансууд",
|
||||||
empty: "Холбосон данс алга",
|
empty: "Холбосон данс алга",
|
||||||
|
emptyHint: "Банкны дансаа холбомогц энд харагдана",
|
||||||
},
|
},
|
||||||
manualAssets: {
|
manualAssets: {
|
||||||
title: "Хөрөнгийн жагсаалт",
|
title: "Хөрөнгийн жагсаалт",
|
||||||
|
|
@ -65,6 +66,7 @@ export const assetsStrings = {
|
||||||
lending: {
|
lending: {
|
||||||
title: "Найзуудад өгсөн зээл",
|
title: "Найзуудад өгсөн зээл",
|
||||||
empty: "Зээл бүртгэгдээгүй байна",
|
empty: "Зээл бүртгэгдээгүй байна",
|
||||||
|
emptyHint: "Найздаа зээл өгсөн бол + дарж бүртгээрэй",
|
||||||
add: "Шинэ зээл",
|
add: "Шинэ зээл",
|
||||||
total: "нийт",
|
total: "нийт",
|
||||||
statusPaid: "Төлсөн",
|
statusPaid: "Төлсөн",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue