Release 0.2.22: capital chart, Investor View, GP stakes, doc folders
Snapshot commit bringing the uncommitted phase-2 work into version control
together with four new features and the 0.2.22 version bump.
New features:
- Investor capital-over-time chart (value, paid-in, distributions per
quarter), rendered from existing capital-account history.
- Admin Investor View: read-only reconstruction of an investor's portal
(GET /api/users/{id}/investor-view), reusing the investor portal UI.
- Document upload scoped to the selected fund's own investors, with an
explicit upload-target confirmation to prevent mis-attaching.
- GP/mgmt entities gain an Assets tab listing their stakes in the funds
they manage (new entity_stakes table + /api/entities/{id}/stakes).
- Edit-entity form (change type/status/etc.), so GP entities can be
categorized correctly.
Verified: 11/11 backend tests pass; alembic upgrades to head b8c9d0e1f2a3;
frontend tsc + vite build clean; s9pk packs at 0.2.22:0 (x86_64).
Also: ignore .DS_Store and *.s9pk artifacts.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { api, type Entity, type Partner } from "../api";
|
||||
import { formatDate, formatMoneyExact } from "../format";
|
||||
import EntityHeader from "../components/EntityHeader";
|
||||
|
||||
export default function EntityPartners() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [entity, setEntity] = useState<Entity | null>(null);
|
||||
const [partners, setPartners] = useState<Partner[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
const eid = parseInt(id);
|
||||
Promise.all([api.getEntity(eid), api.listPartners(eid)])
|
||||
.then(([e, p]) => {
|
||||
setEntity(e);
|
||||
setPartners(p);
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (loading || !entity) return <div className="text-gray-500 text-sm">Loading…</div>;
|
||||
|
||||
const totalCommitted = partners.reduce((s, p) => s + (p.latest_commitment_cents || 0), 0);
|
||||
const totalCapital = partners.reduce((s, p) => s + (p.latest_value_cents || 0), 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EntityHeader entity={entity} active="partners" />
|
||||
|
||||
{error && <p className="text-sm text-red-600 mb-3">{error}</p>}
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm text-gray-500">
|
||||
{partners.length} member{partners.length === 1 ? "" : "s"} with access to this fund.
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
Total committed: <span className="font-medium">{formatMoneyExact(totalCommitted)}</span>
|
||||
<span className="mx-2 text-gray-300">·</span>
|
||||
Total capital: <span className="font-medium">{formatMoneyExact(totalCapital)}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-lg overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 text-gray-500 text-left">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-medium">Member</th>
|
||||
<th className="px-4 py-2 font-medium">Investor ID</th>
|
||||
<th className="px-4 py-2 font-medium">Login</th>
|
||||
<th className="px-4 py-2 font-medium">As of</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Committed</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Paid-in</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Distributions</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Capital value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{partners.map((p) => (
|
||||
<tr key={p.user_id} className="border-t border-gray-100">
|
||||
<td className="px-4 py-2">
|
||||
<div className="text-gray-900">{p.name}</div>
|
||||
<div className="text-xs text-gray-400">{p.username}</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-600">{p.external_investor_id ?? "—"}</td>
|
||||
<td className="px-4 py-2">
|
||||
{!p.is_active ? (
|
||||
<span className="text-gray-400">Disabled</span>
|
||||
) : p.login_enabled ? (
|
||||
<span className="text-green-600">Active</span>
|
||||
) : (
|
||||
<span className="text-amber-600">No login yet</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500">
|
||||
{p.latest_as_of ? formatDate(p.latest_as_of) : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-gray-900">
|
||||
{p.latest_commitment_cents != null ? formatMoneyExact(p.latest_commitment_cents) : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-gray-600">
|
||||
{p.latest_contributions_cents != null ? formatMoneyExact(p.latest_contributions_cents) : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-gray-600">
|
||||
{p.latest_distributions_cents != null ? formatMoneyExact(p.latest_distributions_cents) : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-gray-900">
|
||||
{p.latest_value_cents != null ? formatMoneyExact(p.latest_value_cents) : "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{partners.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-4 py-6 text-center text-gray-400">
|
||||
No members yet. Import the eNAV ALLOC SI tab from “Import Statements,” or grant
|
||||
access on the Access Grid.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user