import { useMemo, useState } from "react"; import { api, type CapitalAccount, type Entity, type PortalDocument } from "../api"; import { categoryLabel, formatDate, formatMoneyExact } from "../format"; import CapitalChart, { type CapitalPoint } from "../components/CapitalChart"; // The investor-facing portal, rendered purely from data. Used by the investor's own home // (InvestorHome) and by the admin read-only Investor View, so both show exactly the same thing. export default function InvestorPortalView({ entities, accounts, docs, }: { entities: Entity[]; accounts: CapitalAccount[]; docs: PortalDocument[]; }) { if (entities.length === 0) { return (

No fund access yet.

); } // When this login covers several legal names (e.g. an IRA and a trust), label each block. const showNames = new Set(accounts.map((a) => a.investor_user_id)).size > 1; return (
{entities.map((e) => ( a.entity_id === e.id)} docs={docs.filter((d) => d.entity_id === e.id)} showNames={showNames} /> ))}
); } function FundSection({ entity, accounts, docs, showNames, }: { entity: Entity; accounts: CapitalAccount[]; docs: PortalDocument[]; showNames: boolean; }) { const byName = useMemo(() => { const groups = new Map(); for (const a of accounts) { const g = groups.get(a.investor_user_id) ?? []; g.push(a); groups.set(a.investor_user_id, g); } return [...groups.values()]; }, [accounts]); return (

{entity.name}

{entity.type}
{byName.length === 0 ? (

No capital account statement on file yet.

) : ( byName.map((group, i) => ( 0} /> )) )}

Documents

{docs.length === 0 ? (

No documents available.

) : (
    {docs.map((d) => (
  • {d.title} {categoryLabel(d.category)} {formatDate(d.created_at)} Download
  • ))}
)}
); } function CapitalBlock({ accounts, label, divider, }: { accounts: CapitalAccount[]; label: string | null; divider: boolean; }) { // accounts arrive newest-first; history is oldest-first for the chart/table. const latest = accounts[0]; const history = useMemo( () => [...accounts].sort((a, b) => a.as_of_date.localeCompare(b.as_of_date)), [accounts], ); const chartPoints: CapitalPoint[] = history.map((a) => ({ date: a.as_of_date, value: a.ending_balance_cents, paidIn: a.contributions_cents, distributions: a.distributions_cents, })); // Collapsed by default so the portal opens clean; the investor expands the trend per fund. const [showChart, setShowChart] = useState(false); return (
{label &&

{label}

}

As of {formatDate(latest.as_of_date)}

{latest.distributions_cents > 0 && ( )} {latest.distributions_cents > 0 && latest.contributions_cents > 0 && ( )}

Current capital value

{formatMoneyExact(latest.ending_balance_cents)}

{history.length > 1 && (
{showChart && (
{history.map((a) => ( ))}
As of Paid-in Distributions Ending balance
{formatDate(a.as_of_date)} {formatMoneyExact(a.contributions_cents)} {formatMoneyExact(a.distributions_cents)} {formatMoneyExact(a.ending_balance_cents)}
)}
)}
); } function Metric({ label, value }: { label: string; value: string }) { return (

{label}

{value}

); }