0.2.27-0.2.32: LP portal polish, brand palette, default investor logins

Cumulative checkpoint since 0.2.26:
- 0.2.27/28: entity valuation-history table; investor gain/loss = NAV +
  distributions vs paid-in
- 0.2.29: Reset Fund Partners (endpoint, Partners-tab button, CLI, action)
- 0.2.30: "Current Capital Balance" label, %-only gain/loss
- 0.2.31: Management Entities rename, Carry Vehicle type, chart
  distributions-line gate
- 0.2.32: LP-facing polish pass
  * Ten31 brand palette from the logo (navy/mint); orange retired
  * portfolio summary card across funds; gain labeled "net of paid-in"
  * whole-dollar headline figures; "History · N quarters" toggle
  * documents grouped by year with a "New" badge (users.docs_seen_at)
  * eNAV-created members start on default password with login enabled;
    enable-investor-logins CLI + StartOS action for existing accounts
  * password minimum raised to 8 chars; login help line (Portal@ten31.xyz)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-03 08:40:40 -05:00
co-authored by Claude Opus 4.8
parent 69f12b0519
commit 4215d4478f
47 changed files with 935 additions and 111 deletions
+52 -6
View File
@@ -1,16 +1,22 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { api, type Entity, type Partner } from "../api";
import { api, canEditRound, type Entity, type Partner } from "../api";
import { useAuth } from "../context/AuthContext";
import { formatDate, formatMoneyExact } from "../format";
import EntityHeader from "../components/EntityHeader";
export default function EntityPartners() {
const { id } = useParams<{ id: string }>();
const { user } = useAuth();
const [entity, setEntity] = useState<Entity | null>(null);
const [partners, setPartners] = useState<Partner[]>([]);
const [error, setError] = useState("");
const [notice, setNotice] = useState("");
const [clearing, setClearing] = useState(false);
const [loading, setLoading] = useState(true);
const isWriter = !!user && canEditRound(user.role);
useEffect(() => {
if (!id) return;
const eid = parseInt(id);
@@ -23,6 +29,34 @@ export default function EntityPartners() {
.finally(() => setLoading(false));
}, [id]);
async function clearAllPartners() {
if (!entity) return;
if (
!confirm(
`Remove ALL ${partners.length} partner(s) from ${entity.name}?\n\n` +
"This deletes this fund's capital-account statements and the investors' access to " +
"it. The investor accounts themselves are kept (they may belong to other funds), " +
"and holdings/NAV are not affected. Re-import the correct roster afterward.",
)
)
return;
setClearing(true);
setError("");
setNotice("");
try {
const res = await api.clearPartners(entity.id);
const fresh = await api.listPartners(entity.id);
setPartners(fresh);
setNotice(
`Cleared: removed ${res.statements} capital statement(s) and ${res.access_grants} access grant(s).`,
);
} catch (err: any) {
setError(err.message || "Failed to clear partners");
} finally {
setClearing(false);
}
}
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);
@@ -33,16 +67,28 @@ export default function EntityPartners() {
<EntityHeader entity={entity} active="partners" />
{error && <p className="text-sm text-red-600 mb-3">{error}</p>}
{notice && <p className="text-sm text-green-600 mb-3">{notice}</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 className="flex items-center gap-4">
<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>
{isWriter && partners.length > 0 && (
<button
onClick={clearAllPartners}
disabled={clearing}
className="px-3 py-1.5 border border-red-300 text-red-600 text-sm rounded hover:bg-red-50 disabled:opacity-50"
>
{clearing ? "Clearing…" : "Clear all partners"}
</button>
)}
</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg overflow-x-auto">