import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { api, type EntityType } from "../api"; import { useAuth } from "../context/AuthContext"; import { formatMoney, formatGainLoss } from "../format"; interface EntityRow { id: number; name: string; type: string; vintage_year: number | null; fund_size_cents: number | null; status: string; investedCents: number; lastValueCents: number; } const TYPE_LABELS: Record = { fund: "Fund", spv: "SPV", gp: "GP", mgmt_co: "Mgmt Co", }; const WRITER_ROLES = ["fund_admin", "cfo", "approver"]; export default function EntitiesList() { const { user } = useAuth(); const [entities, setEntities] = useState([]); const [loading, setLoading] = useState(true); const [showForm, setShowForm] = useState(false); const isWriter = user && WRITER_ROLES.includes(user.role); useEffect(() => { loadData(); }, []); async function loadData() { setLoading(true); try { const rollup = await api.listEntityRollup(); setEntities( rollup.map((r) => ({ id: r.id, name: r.name, type: r.type, vintage_year: r.vintage_year, fund_size_cents: r.fund_size_cents, status: r.status, investedCents: r.invested_cents, lastValueCents: r.last_signed_value_cents, })), ); } finally { setLoading(false); } } if (loading) { return
Loading...
; } const funds = entities.filter((e) => e.type === "fund" || e.type === "spv"); const gps = entities.filter((e) => e.type === "gp" || e.type === "mgmt_co"); return (

Entities

{isWriter && ( )}
{showForm && ( setShowForm(false)} onCreated={() => { setShowForm(false); loadData(); }} /> )}
); } function AddEntityForm({ onClose, onCreated, }: { onClose: () => void; onCreated: () => void; }) { const [name, setName] = useState(""); const [type, setType] = useState("fund"); const [vintageYear, setVintageYear] = useState(""); const [fundSizeDollars, setFundSizeDollars] = useState(""); const [error, setError] = useState(""); const [saving, setSaving] = useState(false); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!name.trim()) { setError("Name is required."); return; } setError(""); setSaving(true); try { const data: any = { name: name.trim(), type }; if (vintageYear) { const vy = parseInt(vintageYear); if (isNaN(vy)) { setError("Vintage year must be a number."); setSaving(false); return; } data.vintage_year = vy; } if (fundSizeDollars) { const dollars = parseFloat(fundSizeDollars.replace(/[,$]/g, "")); if (isNaN(dollars)) { setError("Fund size must be a number."); setSaving(false); return; } data.fund_size_cents = Math.round(dollars * 100); } await api.createEntity(data); onCreated(); } catch (err: any) { setError(err.message || "Failed to create entity"); } finally { setSaving(false); } } return (

New Entity

setName(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent" placeholder="Low Time Preference Fund I, LLC" required />
setVintageYear(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded text-sm" placeholder="2021" />
setFundSizeDollars(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded text-sm" placeholder="3,300,000" />
{error && (

{error}

)}
); } function EntityTable({ title, rows }: { title: string; rows: EntityRow[] }) { const totalInvested = rows.reduce((s, r) => s + r.investedCents, 0); const totalValue = rows.reduce((s, r) => s + r.lastValueCents, 0); const totalGain = totalValue - totalInvested; return (

{title}

{rows.map((row) => { const gl = formatGainLoss(row.lastValueCents, row.investedCents); return ( ); })} {rows.length > 0 && ( )} {rows.length === 0 && ( )}
Name Type Vintage Invested Last Signed Value Gain/Loss
{row.name} {TYPE_LABELS[row.type] || row.type} {row.vintage_year || "—"} {formatMoney(row.investedCents)} {formatMoney(row.lastValueCents)} {gl.positive ? "+" : "-"}{gl.text}
Total {formatMoney(totalInvested)} {formatMoney(totalValue)} = 0 ? "text-green-600" : "text-red-600"}`}> {totalGain >= 0 ? "+" : "-"}{formatMoney(Math.abs(totalGain))}
No entities yet.
); }