import { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { SegmentedControl } from "@/components/charts/SegmentedControl"; import { shareColor } from "@/components/dashboard/shareColors"; import { useAllInvestmentAccountTags } from "@/hooks/useTags"; import { tagColorValue } from "@/lib/tagColors"; import { usePrivacy } from "@/lib/privacy"; import { formatMoney } from "@/lib/format"; import { cn } from "@/lib/utils"; import type { TagDto } from "@/lib/api"; export interface ConnectedAccountRow { id: string; label: string; to: string; value: number; pct: number; currency: string; /** The account's own currency, when its value is shown converted into another. */ nativeCurrency?: string; connected: boolean; isManual: boolean; actions?: React.ReactNode; } type SortMode = "value" | "name"; const SORT_OPTIONS: { value: SortMode; label: string }[] = [ { value: "value", label: "Value" }, { value: "name", label: "Name" }, ]; function fmt(v: number, c: string) { return formatMoney(v, c, { decimals: 0 }); } function initials(label: string): string { const words = label.trim().split(/\s+/).filter(Boolean); if (words.length === 0) return "?"; if (words.length === 1) return words[0].slice(0, 2).toUpperCase(); return (words[0][0] + words[1][0]).toUpperCase(); } /** * Ranked, information-dense list of every connected/manual source, replacing * a grid of big cards: at a glance, which source is biggest and what share * of the total it is, the two things a card forces you to compare by eye. * * Sort (Value/Name) and an optional tag filter give two independent ways to * organize the list once it's got more than a handful of accounts in it. */ export function ConnectedAccountsList({ accounts, total, currency }: { accounts: ConnectedAccountRow[]; total: number; currency: string }) { usePrivacy(); const [sortMode, setSortMode] = useState("value"); const [activeTagId, setActiveTagId] = useState(null); const accountIds = useMemo(() => accounts.map((a) => a.id), [accounts]); const tagsById = useAllInvestmentAccountTags(accountIds); const allTags = useMemo(() => { const seen = new Map(); Object.values(tagsById).forEach((tags) => tags.forEach((t) => seen.set(t.id, t))); return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name)); }, [tagsById]); const filtered = activeTagId ? accounts.filter((a) => (tagsById[a.id] ?? []).some((t) => t.id === activeTagId)) : accounts; const sorted = useMemo(() => { const rows = [...filtered]; if (sortMode === "name") rows.sort((a, b) => a.label.localeCompare(b.label)); else rows.sort((a, b) => b.value - a.value); return rows; }, [filtered, sortMode]); return (
Connected accounts {accounts.length}
{accounts.length > 1 && (
)} {allTags.length > 0 && (
{allTags.map((t) => { const active = activeTagId === t.id; return ( ); })}
)}
{sorted.length === 0 ? (

{activeTagId ? "No accounts with that tag." : "No accounts yet."}

) : ( sorted.map((a, i) => { const tags = tagsById[a.id] ?? []; return (
{initials(a.label)}

{a.label}

{a.isManual ? "Manual" : a.connected ? "Connected" : "Disconnected"} · {a.nativeCurrency ?? a.currency} {tags.map((t) => ( {t.name} ))}

{fmt(a.value, a.currency)}

{total > 0 ? `${a.pct.toFixed(1)}%` : "—"}

{a.actions &&
{a.actions}
} ); }) )}
); }