diff --git a/src/components/frontend/banking/AccountCard.tsx b/src/components/frontend/banking/AccountCard.tsx index 1a50111..69ada47 100644 --- a/src/components/frontend/banking/AccountCard.tsx +++ b/src/components/frontend/banking/AccountCard.tsx @@ -12,6 +12,11 @@ import { } from "@/components/ui/item"; import { LandmarkIcon, UsersIcon, WalletIcon } from "lucide-react"; import { formatAmount } from "@/lib/banking/format"; +import { + accountStatusBase, + accountStatusClass, + type AccountStatus, +} from "@/lib/banking/ui"; const accountTypeMeta = { treasury: { @@ -38,12 +43,6 @@ export function accountTypeInfo(accountType: string) { return accountTypeMeta[accountType as keyof typeof accountTypeMeta] ?? accountTypeMeta.personal; } -const statusColor: Record = { - open: "bg-green-500/20 text-green-400", - frozen: "bg-amber-500/20 text-amber-400", - closed: "bg-muted text-muted-foreground", -}; - export function AccountCard({ account }: { account: BankAccount }) { const meta = accountTypeInfo(account.accountType); const Icon = meta.icon; @@ -58,6 +57,8 @@ export function AccountCard({ account }: { account: BankAccount }) { ? account.ownerFaction?.name : null; + const statusKey = (account.status ?? "open") as AccountStatus; + return ( {account.name} - {account.status} + {statusKey} diff --git a/src/components/frontend/banking/AccountDetail.tsx b/src/components/frontend/banking/AccountDetail.tsx index c4f9aa3..00e4013 100644 --- a/src/components/frontend/banking/AccountDetail.tsx +++ b/src/components/frontend/banking/AccountDetail.tsx @@ -10,16 +10,15 @@ import { ItemTitle, } from "@/components/ui/item"; import { formatAmount } from "@/lib/banking/format"; +import { + accountStatusBase, + accountStatusClass, + type AccountStatus, +} from "@/lib/banking/ui"; import { accountTypeInfo } from "@/components/frontend/banking/AccountCard"; import { BankTransactionDialog } from "@/components/frontend/banking/BankTransactionDialog"; import { LedgerTable } from "@/components/frontend/banking/LedgerTable"; -const statusColor: Record = { - open: "bg-green-500/20 text-green-400", - frozen: "bg-amber-500/20 text-amber-400", - closed: "bg-muted text-muted-foreground", -}; - interface AccountDetailProps { account: BankAccount; ledger: LedgerEntry[]; @@ -36,6 +35,7 @@ export function AccountDetail({ const meta = accountTypeInfo(account.accountType); const Icon = meta.icon; const currency = typeof account.currency === "object" ? account.currency : null; + const statusKey = (account.status ?? "open") as AccountStatus; const owner = account.accountType === "personal" @@ -64,9 +64,9 @@ export function AccountDetail({
- {account.status} + {statusKey} {meta.label} @@ -75,7 +75,7 @@ export function AccountDetail({
-

+

Balance

diff --git a/src/components/frontend/banking/LedgerTable.tsx b/src/components/frontend/banking/LedgerTable.tsx index 599d91f..45ac2da 100644 --- a/src/components/frontend/banking/LedgerTable.tsx +++ b/src/components/frontend/banking/LedgerTable.tsx @@ -2,23 +2,26 @@ import type { LedgerEntry } from "@/payload-types"; import { formatDate } from "@/lib/banking/format"; +import { txAmountClass, txAmountPrefix, txTypeColor, txTypeFlow } from "@/lib/banking/ui"; -const typeMeta: Record = { - deposit: { label: "Deposit", cls: "text-emerald-400" }, - withdrawal: { label: "Withdrawal", cls: "text-red-400" }, - transfer: { label: "Transfer", cls: "text-blue-400" }, - payment: { label: "Payment", cls: "text-red-400" }, - fee: { label: "Fee", cls: "text-muted-foreground" }, - salary: { label: "Salary", cls: "text-green-400" }, - adjustment: { label: "Adjustment", cls: "text-amber-400" }, +// Title-case label per type — kept local because the table column header is +// "Type" (title case) and matches title-case values; the ActivityFeed uses +// uppercase mono labels per the terminal-log aesthetic. Each surface keeps +// the case that fits its chrome. +const txTypeLabel: Record = { + deposit: "Deposit", + withdrawal: "Withdrawal", + transfer: "Transfer", + payment: "Payment", + fee: "Fee", + salary: "Salary", + adjustment: "Adjustment", }; export function LedgerTable({ entries }: { entries: LedgerEntry[] }) { if (entries.length === 0) { return ( -

- No ledger entries yet. -

+

No ledger entries yet.

); } @@ -27,44 +30,51 @@ export function LedgerTable({ entries }: { entries: LedgerEntry[] }) { - - - - - {entries.map((entry) => { - const meta = typeMeta[entry.type] ?? { - label: entry.type, - cls: "text-muted-foreground", - }; + const colorCls = txTypeColor[entry.type] ?? "text-muted-foreground"; + const label = txTypeLabel[entry.type] ?? entry.type; + const flow = txTypeFlow[entry.type] ?? "neutral"; const amount = entry.amount ?? 0; + const absAmount = Math.abs(amount); + // If the entry's own sign disagrees with the canonical flow (e.g. + // a transfer into this account is stored as +amount), trust the + // entry's actual sign — it reflects the perspective of THIS + // account, which the canonical flow cannot know. + const useEntrySign = amount !== 0 && Math.sign(amount) < 0; + const prefix = useEntrySign + ? txAmountPrefix("out") + : txAmountPrefix(flow); + const amountCls = useEntrySign + ? txAmountClass("out") + : txAmountClass(flow); return ( - + -
+ Date + Type + Memo + Amount + Balance
{formatDate(entry.timestamp)} - {meta.label} - {label} {entry.memo}= 0 ? "text-emerald-400" : "text-red-400"}`} - > - {amount >= 0 ? "+" : ""} - {amount.toLocaleString()} + + {prefix} + {absAmount.toLocaleString()} {(entry.balanceAfter ?? 0).toLocaleString()} @@ -76,4 +86,4 @@ export function LedgerTable({ entries }: { entries: LedgerEntry[] }) {
); -} +} \ No newline at end of file diff --git a/src/lib/banking/ui.ts b/src/lib/banking/ui.ts new file mode 100644 index 0000000..7155a40 --- /dev/null +++ b/src/lib/banking/ui.ts @@ -0,0 +1,111 @@ +/** + * Shared visual tokens for the banking UI family. + * + * Used by: + * - WalletHero, AccountCard, AccountDetail (account status chips) + * - ActivityFeed, LedgerTable (transaction type colors and amount signs) + * - BankTransactionDialog (trigger button styling via shared className) + * + * Editing any value here updates every banking component consistently. + * Keep tokens minimal and semantic — no one-off custom classes here. + */ + +// --- Account status chip ----------------------------------------------------- + +export type AccountStatus = "open" | "frozen" | "closed"; + +export const accountStatusClass: Record = { + open: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30", + frozen: "bg-amber-500/15 text-amber-400 border-amber-500/30", + closed: "bg-muted text-muted-foreground border-border", +}; + +// Shared base class for the status chip — applied alongside accountStatusClass. +export const accountStatusBase = + "text-[10px] font-mono font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded border"; + +// --- Transaction type tokens ------------------------------------------------- + +// Tailwind text-color class per transaction type — used by ActivityFeed and +// LedgerTable so the two transaction displays share one color source of truth. +export const txTypeColor: Record = { + deposit: "text-emerald-400", + withdrawal: "text-red-400", + transfer: "text-blue-400", + payment: "text-red-400", + fee: "text-muted-foreground", + salary: "text-emerald-400", + adjustment: "text-amber-400", +}; + +// Flow direction — drives amount sign and color. Mirrors txTypeColor semantics +// but groups types by their effect on the account being viewed. +export type TxFlow = "in" | "out" | "neutral"; + +export const txTypeFlow: Record = { + deposit: "in", + withdrawal: "out", + transfer: "neutral", + payment: "out", + fee: "out", + salary: "in", + adjustment: "neutral", +}; + +export function txAmountClass(flow: TxFlow): string { + if (flow === "in") return "text-emerald-400"; + if (flow === "out") return "text-red-400"; + return "text-blue-400"; +} + +export function txAmountPrefix(flow: TxFlow): string { + if (flow === "in") return "+"; + if (flow === "out") return "\u2212"; // typographic minus (U+2212), aligns with mono digits + return ""; +} + +// --- Timestamp formatters ----------------------------------------------------- + +/** + * Compact 24h timestamp for transaction feed rows: + * - today → "HH:MM" (24h, local time) + * - older → "MMM DD" (uppercase, local time) + * + * Used by ActivityFeed. Keep in sync with formatLastActivity below — both + * read local time, never UTC, so a transaction rendered in the feed and the + * "last activity" line on the hero never disagree by a timezone offset. + */ +export function formatTxTimestamp(ts: string): string { + const date = new Date(ts); + if (Number.isNaN(date.getTime())) return "—"; + const now = new Date(); + const isToday = date.toDateString() === now.toDateString(); + if (isToday) { + const hh = String(date.getHours()).padStart(2, "0"); + const mm = String(date.getMinutes()).padStart(2, "0"); + return `${hh}:${mm}`; + } + return date + .toLocaleDateString("en-US", { month: "short", day: "numeric" }) + .toUpperCase(); +} + +/** + * Absolute "last activity" timestamp for the WalletHero: + * "YYYY-MM-DD HH:MM" in LOCAL time (not UTC). + * + * The previous implementation used `date.toISOString()` which rendered UTC, + * causing the hero's "LAST ACTIVITY" line to be hours ahead of the same + * transaction rendered in ActivityFeed (which reads local time). + */ +export function formatLastActivity(ts?: string): string { + if (!ts) return "NO ACTIVITY YET"; + const date = new Date(ts); + if (Number.isNaN(date.getTime())) return "NO ACTIVITY YET"; + const yyyy = date.getFullYear(); + const mm = String(date.getMonth() + 1).padStart(2, "0"); + const dd = String(date.getDate()).padStart(2, "0"); + const hh = String(date.getHours()).padStart(2, "0"); + const min = String(date.getMinutes()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd} ${hh}:${min}`; +} \ No newline at end of file