import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { api, type Entity, type Holding, type Position, type ValuationRound, type Valuation } from "../api"; import { formatMoney, formatMoneyExact, formatDate, formatQuarter, formatPerShare } from "../format"; const TYPE_LABELS: Record = { fund: "Fund", spv: "SPV", gp: "GP", mgmt_co: "Mgmt Co", }; interface PositionWithValuation extends Position { lastValueCents: number | null; valuationDate: string | null; valuationQuarter: string | null; } interface HoldingGroup { holding: Holding; positions: PositionWithValuation[]; totalCost: number; totalValue: number; } export default function Investments() { const { id } = useParams<{ id: string }>(); const [entity, setEntity] = useState(null); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { if (!id) return; loadData(parseInt(id)); }, [id]); async function loadData(entityId: number) { setLoading(true); try { const ent = await api.getEntity(entityId); setEntity(ent); const holdings = await api.listHoldings(entityId); const rounds = await api.listRounds(entityId); // Find latest approved round const approvedRounds = rounds .filter((r) => r.status === "approved") .sort((a, b) => b.quarter_end.localeCompare(a.quarter_end)); const latestApproved = approvedRounds[0] || null; // Build valuation lookup: position_id -> {value_cents, quarter_end, approved_at} const valMap = new Map(); if (latestApproved) { for (const v of latestApproved.valuations) { valMap.set(v.position_id, { value_cents: v.value_cents, quarter_end: latestApproved.quarter_end, approved_at: latestApproved.approved_at, }); } } const result: HoldingGroup[] = []; for (const h of holdings) { const positions = await api.listPositions(h.id); const enriched: PositionWithValuation[] = positions.map((p) => { const val = valMap.get(p.id); return { ...p, lastValueCents: val?.value_cents ?? null, valuationDate: val?.approved_at ?? null, valuationQuarter: val?.quarter_end ?? null, }; }); const totalCost = enriched.reduce((s, p) => s + p.cost_cents, 0); const totalValue = enriched.reduce((s, p) => s + (p.lastValueCents ?? 0), 0); result.push({ holding: h, positions: enriched, totalCost, totalValue }); } setGroups(result); } finally { setLoading(false); } } if (loading || !entity) { return
Loading...
; } const totalCost = groups.reduce((s, g) => s + g.totalCost, 0); const totalValue = groups.reduce((s, g) => s + g.totalValue, 0); const totalGain = totalValue - totalCost; const positionCount = groups.reduce((s, g) => s + g.positions.length, 0); return (
{/* Header */}

{entity.name}

{TYPE_LABELS[entity.type] || entity.type}
Overview Investments Partners Documents
{/* Header band */}
Active investments:{" "} {positionCount}
Total cost:{" "} {formatMoney(totalCost)}
Total value:{" "} {formatMoney(totalValue)}
Unrealized gain/loss:{" "} = 0 ? "text-green-600" : "text-red-600"}`}> {totalGain >= 0 ? "+" : "-"}{formatMoney(Math.abs(totalGain))}
{/* Grouped table */}
{groups.map((group) => { const holdingGain = group.totalValue - group.totalCost; return ( ); })}
Security Date Shares Cost Cost/Share Signed Value Value/Share Signed Gain/Loss
); } function HoldingGroupRows({ group, holdingGain }: { group: HoldingGroup; holdingGain: number }) { return ( <> {/* Holding header row */} {group.holding.company_name} {formatMoney(group.totalCost)} {formatMoney(group.totalValue)} = 0 ? "text-green-600" : "text-red-600"}`}> {holdingGain >= 0 ? "+" : "-"}{formatMoney(Math.abs(holdingGain))} {/* Position rows */} {group.positions.map((pos) => { const gain = (pos.lastValueCents ?? 0) - pos.cost_cents; return ( {pos.security_name} {formatDate(pos.investment_date)} {pos.shares ? parseFloat(pos.shares).toLocaleString() : "—"} {formatMoney(pos.cost_cents)} {formatPerShare(pos.cost_cents, pos.shares)} {pos.lastValueCents != null ? formatMoney(pos.lastValueCents) : "—"} {pos.lastValueCents != null ? formatPerShare(pos.lastValueCents, pos.shares) : "—"} {pos.valuationQuarter ? `${formatQuarter(pos.valuationQuarter)} ${pos.valuationDate ? formatDate(pos.valuationDate) : ""}` : "—"} = 0 ? "text-green-600" : "text-red-600"}`}> {pos.lastValueCents != null ? ( <> {gain >= 0 ? "▲" : "▼"} {formatMoney(Math.abs(gain))} ) : ( "—" )} ); })} ); }