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:
co-authored by
Claude Opus 4.8
parent
69f12b0519
commit
4215d4478f
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user