0.2.25: batch historical eNAV backfill + collapsible investor chart

Add POST /api/import/capital-accounts/batch: upload several of a fund's
eNAV workbooks at once; each file's ALLOC SI roster is auto-matched to
existing members (by fund-admin investor ID, else name/username) and their
capital statement is saved at that file's own as-of date, building
trend-lines without replacing the latest figures. Members not already in
the portal are skipped and reported per file (never created). Capital
statements only -- holdings/NAV are untouched. One bad file (wrong
password, no ALLOC SI, unreadable date) is reported per-file and does not
abort the rest.

Import page gains a "Backfill historical capital" batch section (fund
picker, multi-file .xlsx input, shared password, per-file results table).

Investor portal "Capital over time" chart is now collapsed by default and
expands per fund (first login opens clean); applies to InvestorHome and
the admin Investor View via the shared component.

Tests: backend/tests/test_capital_batch.py (2). Full suite 17 passed;
frontend tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-01 18:47:01 -05:00
co-authored by Claude Opus 4.8
parent 33776d42f4
commit 099459b2f3
10 changed files with 447 additions and 8 deletions
+19 -2
View File
@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { api, type CapitalAccount, type Entity, type PortalDocument } from "../api";
import { categoryLabel, formatDate, formatMoneyExact } from "../format";
import CapitalChart, { type CapitalPoint } from "../components/CapitalChart";
@@ -125,6 +125,8 @@ function CapitalBlock({
paidIn: a.contributions_cents,
distributions: a.distributions_cents,
}));
// Collapsed by default so the portal opens clean; the investor expands the trend per fund.
const [showChart, setShowChart] = useState(false);
return (
<div className={divider ? "mt-6 pt-5 border-t border-gray-100" : "mt-3"}>
@@ -150,7 +152,20 @@ function CapitalBlock({
{history.length > 1 && (
<div className="mt-5">
<h3 className="text-xs font-medium text-gray-500 uppercase mb-2">Capital over time</h3>
<button
type="button"
onClick={() => setShowChart((v) => !v)}
aria-expanded={showChart}
className="flex items-center gap-1.5 text-xs font-medium text-gray-500 uppercase hover:text-gray-700"
>
<span className={`transition-transform ${showChart ? "rotate-90" : ""}`}></span>
Capital over time
<span className="normal-case font-normal text-gray-400">
({history.length} quarters{showChart ? "" : " — show"})
</span>
</button>
{showChart && (
<div className="mt-3">
<CapitalChart points={chartPoints} />
<table className="w-full text-sm mt-4">
<thead className="text-gray-400 text-left">
@@ -172,6 +187,8 @@ function CapitalBlock({
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>