import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; 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(null); const [partners, setPartners] = useState([]); const [error, setError] = useState(""); const [notice, setNotice] = useState(""); const [clearing, setClearing] = useState(false); const [loading, setLoading] = useState(true); // Row currently getting an exit date (inline date picker), and the chosen date. const [exitingId, setExitingId] = useState(null); const [exitDate, setExitDate] = useState(() => new Date().toISOString().slice(0, 10)); const isWriter = !!user && canEditRound(user.role); 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]); 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); } } async function saveExit(userId: number, exitedOn: string | null) { if (!entity) return; setError(""); try { const updated = await api.setPartnerExited(entity.id, userId, exitedOn); setPartners((prev) => prev.map((p) => (p.user_id === userId ? updated : p))); setExitingId(null); } catch (err: any) { setError(err.message || "Failed to update exit status"); } } if (loading || !entity) return
Loading…
; // Exited members (stake sold/transferred) stay on the roster but out of the totals — // otherwise a seller and their buyer would both be counted. const active = partners.filter((p) => !p.exited_on); const exitedCount = partners.length - active.length; const totalCommitted = active.reduce((s, p) => s + (p.latest_commitment_cents || 0), 0); const totalCapital = active.reduce((s, p) => s + (p.latest_value_cents || 0), 0); return (
{error &&

{error}

} {notice &&

{notice}

}

{partners.length} member{partners.length === 1 ? "" : "s"} with access to this fund.

Total committed: {formatMoneyExact(totalCommitted)} · Total capital: {formatMoneyExact(totalCapital)} {exitedCount > 0 && ( (excludes {exitedCount} exited) )}

{isWriter && partners.length > 0 && ( )}
{partners.map((p) => ( ))} {partners.length === 0 && ( )}
Member Investor ID Login Status As of Committed Paid-in Distributions Capital value
{p.name}
{p.username}
{p.external_investor_id ?? "—"} {!p.is_active ? ( Disabled ) : p.login_enabled ? ( Active ) : ( No login yet )} {p.exited_on ? ( Exited {formatDate(p.exited_on)} {isWriter && ( )} ) : exitingId === p.user_id ? ( setExitDate(e.target.value)} className="px-1.5 py-0.5 border border-gray-300 rounded text-xs" /> ) : ( Member {isWriter && ( )} )} {p.latest_as_of ? formatDate(p.latest_as_of) : "—"} {p.latest_commitment_cents != null ? formatMoneyExact(p.latest_commitment_cents) : "—"} {p.latest_contributions_cents != null ? formatMoneyExact(p.latest_contributions_cents) : "—"} {p.latest_distributions_cents != null ? formatMoneyExact(p.latest_distributions_cents) : "—"} {p.latest_value_cents != null ? formatMoneyExact(p.latest_value_cents) : "—"}
No members yet. Import the eNAV ALLOC SI tab from “Import Statements,” or grant access on the Access Grid.
); }