0.2.35: exited positions show history clipped at the exit date

The eNAV keeps producing statements after an exit (quarterly for funds,
event-driven for SPVs), so an unfiltered graph would crash to zero.
Now the exited card gets its collapsible History back — chart + table
showing the capital journey up to exited_on only. Post-exit statements
stay recorded (audit trail; Undo restores full history) but are never
plotted. History section extracted into a shared HistorySection
component; toggle counts "statements" not "quarters" (SPV statements
are event-driven).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-03 09:23:47 -05:00
co-authored by Claude Opus 4.8
parent 6313d781b4
commit 2c87ab499a
6 changed files with 59 additions and 32 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
// - content-hashed /assets/* are cache-first (immutable, safe forever)
// - /api/* is never cached
// Bump CACHE on each release so old entries are purged.
const CACHE = 'ten31-portal-0.2.34'
const CACHE = 'ten31-portal-0.2.35'
self.addEventListener('install', () => self.skipWaiting())
+40 -27
View File
@@ -256,18 +256,13 @@ function CapitalBlock({
() => [...accounts].sort((a, b) => a.as_of_date.localeCompare(b.as_of_date)),
[accounts],
);
const chartPoints: CapitalPoint[] = history.map((a) => ({
date: a.as_of_date,
value: a.ending_balance_cents,
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);
// A sold/transferred stake: the fund's books show $0 with no distribution, which is not a
// loss. Show a quiet Exited badge and keep the documents; no balance, no gain/loss.
// loss. Show a quiet Exited badge and their capital journey UP TO the exit — statements
// the eNAV keeps producing after that date are recorded but never plotted, so the line
// ends at the exit instead of crashing to zero.
if (latest.exited_on) {
const clipped = history.filter((a) => a.as_of_date <= latest.exited_on!);
return (
<div className={divider ? "mt-6 pt-5 border-t border-gray-100" : "mt-3"}>
{label && <p className="text-sm font-medium text-gray-700">{label}</p>}
@@ -279,6 +274,7 @@ function CapitalBlock({
<p className="text-sm text-gray-400 mt-2">
This position was sold or transferred. Documents remain available below.
</p>
<HistorySection history={clipped} />
</div>
);
}
@@ -314,22 +310,41 @@ function CapitalBlock({
</p>
)}
{history.length > 1 && (
<div className="mt-5">
<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>
History
<span className="normal-case font-normal text-gray-400">
· {history.length} quarters
</span>
</button>
{showChart && (
<div className="mt-3">
<HistorySection history={history} />
</div>
);
}
// Collapsible capital-over-time chart + statement table. Collapsed by default so the portal
// opens clean; the investor expands the trend per fund. SPVs get sparse event-driven points,
// funds quarterly ones — the chart plots whatever statements exist.
function HistorySection({ history }: { history: CapitalAccount[] }) {
const [showChart, setShowChart] = useState(false);
if (history.length < 2) return null;
const chartPoints: CapitalPoint[] = history.map((a) => ({
date: a.as_of_date,
value: a.ending_balance_cents,
paidIn: a.contributions_cents,
distributions: a.distributions_cents,
}));
return (
<div className="mt-5">
<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>
History
<span className="normal-case font-normal text-gray-400">
· {history.length} statements
</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">
@@ -351,8 +366,6 @@ function CapitalBlock({
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
// Bumped each release so the running build is visible in the UI.
// If the number shown in the app doesn't match the installed s9pk version,
// the new frontend isn't actually being served.
export const APP_VERSION = "0.2.34";
export const APP_VERSION = "0.2.35";