Issue 1: repo scaffold and project structure

This commit is contained in:
Johnny 5
2026-06-07 19:20:02 +00:00
parent e0b31009b7
commit a70bdeaa5e
52 changed files with 6750 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api, type Entity, type Holding, type Position, type ValuationRound } from "../api";
import { formatMoney, formatGainLoss } from "../format";
interface EntityRow extends Entity {
investedCents: number;
lastValueCents: number;
}
export default function EntitiesList() {
const [entities, setEntities] = useState<EntityRow[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadData();
}, []);
async function loadData() {
setLoading(true);
try {
const ents = await api.listEntities();
const enriched: EntityRow[] = [];
for (const ent of ents) {
const holdings = await api.listHoldings(ent.id);
let investedCents = 0;
let lastValueCents = 0;
// Get all positions for cost
for (const h of holdings) {
const positions = await api.listPositions(h.id);
for (const p of positions) {
investedCents += p.cost_cents;
}
}
// Get latest approved round for value
const rounds = await api.listRounds(ent.id);
const approved = rounds.filter((r) => r.status === "approved");
if (approved.length > 0) {
const latest = approved[0]; // Already sorted desc by quarter_end
lastValueCents = latest.valuations.reduce((sum, v) => sum + v.value_cents, 0);
}
enriched.push({ ...ent, investedCents, lastValueCents });
}
setEntities(enriched);
} finally {
setLoading(false);
}
}
if (loading) {
return <div className="text-gray-500 text-sm">Loading...</div>;
}
const funds = entities.filter((e) => e.type === "fund" || e.type === "spv");
const gps = entities.filter((e) => e.type === "gp" || e.type === "mgmt_co");
return (
<div className="space-y-8">
<EntityTable title="Funds and SPVs" rows={funds} />
<EntityTable title="GP Entities and Management Companies" rows={gps} />
</div>
);
}
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;
const TYPE_LABELS: Record<string, string> = {
fund: "Fund",
spv: "SPV",
gp: "GP",
mgmt_co: "Mgmt Co",
};
return (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">{title}</h2>
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="text-left px-4 py-3 font-medium text-gray-600">Name</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Type</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Vintage</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Invested</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Last Signed Value</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Gain/Loss</th>
</tr>
</thead>
<tbody>
{rows.map((row) => {
const gl = formatGainLoss(row.lastValueCents, row.investedCents);
return (
<tr key={row.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="px-4 py-3">
<Link
to={`/entities/${row.id}`}
className="text-gray-900 font-medium hover:text-orange-600"
>
{row.name}
</Link>
</td>
<td className="px-4 py-3">
<span className="inline-block px-2 py-0.5 bg-gray-100 text-gray-600 text-xs rounded">
{TYPE_LABELS[row.type] || row.type}
</span>
</td>
<td className="px-4 py-3 text-gray-600">{row.vintage_year || "—"}</td>
<td className="px-4 py-3 text-right text-gray-900">{formatMoney(row.investedCents)}</td>
<td className="px-4 py-3 text-right text-gray-900">{formatMoney(row.lastValueCents)}</td>
<td className={`px-4 py-3 text-right font-medium ${gl.positive ? "text-green-600" : "text-red-600"}`}>
{gl.positive ? "+" : "-"}{gl.text}
</td>
</tr>
);
})}
{rows.length > 0 && (
<tr className="bg-gray-50 font-medium">
<td className="px-4 py-3 text-gray-900" colSpan={3}>
Total
</td>
<td className="px-4 py-3 text-right text-gray-900">{formatMoney(totalInvested)}</td>
<td className="px-4 py-3 text-right text-gray-900">{formatMoney(totalValue)}</td>
<td className={`px-4 py-3 text-right ${totalGain >= 0 ? "text-green-600" : "text-red-600"}`}>
{totalGain >= 0 ? "+" : "-"}{formatMoney(Math.abs(totalGain))}
</td>
</tr>
)}
{rows.length === 0 && (
<tr>
<td className="px-4 py-6 text-center text-gray-400" colSpan={6}>
No entities yet.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
}