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
+32
View File
@@ -130,6 +130,22 @@ export interface CapitalImportResult {
statements_written: number;
}
export interface BatchCapitalFileResult {
filename: string;
as_of_date: string | null;
matched: number;
statements_written: number;
updated: number;
skipped: string[];
error: string | null;
}
export interface BatchCapitalImportResult {
entity_id: number;
files: BatchCapitalFileResult[];
total_statements: number;
}
export interface CapitalAccount {
id: number;
entity_id: number;
@@ -440,6 +456,22 @@ export const api = {
method: "POST",
body: JSON.stringify(data),
}),
// Batch backfill: several historical eNAV files → capital statements per quarter, auto-matched.
capitalImportBatch: async (
entityId: number,
files: File[],
password?: string,
): Promise<BatchCapitalImportResult> => {
const form = new FormData();
form.set("entity_id", String(entityId));
if (password) form.set("password", password);
for (const f of files) form.append("files", f);
const res = await fetch("/api/import/capital-accounts/batch", { method: "POST", body: form });
const data = await res.json().catch(() => ({ detail: res.statusText }));
if (!res.ok) throw new ApiError(res.status, data.detail || "Batch import failed");
return data;
},
getUser: (id: number) => request<UserDetail>(`/api/users/${id}`),
createUser: (data: {
name: string;