0.2.34: manage exited status from the Capital Accounts view
The eNAV keeps listing exited members each quarter, so the admin needs the exit control where the statements live: - Capital Accounts table gains a Status column with the same Exited-badge / mark / undo flow as the Partners tab (keyed per investor+fund pair — marking any statement row marks them all) - set_partner_exited now creates the access row (flag set) when a manually-entered investor has statements but no roster entry yet; clearing a never-set exit stays 404 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a639ba14cf
commit
6313d781b4
@@ -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.33'
|
||||
const CACHE = 'ten31-portal-0.2.34'
|
||||
|
||||
self.addEventListener('install', () => self.skipWaiting())
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ export default function CapitalAccounts() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [rows, setRows] = useState<CapitalAccount[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
// Exit status is per investor+fund (from the eNAV the admin keeps re-importing), so the
|
||||
// inline picker is keyed on that pair — marking any statement row marks them all.
|
||||
const [exitingKey, setExitingKey] = useState<string | null>(null);
|
||||
const [exitDate, setExitDate] = useState(() => new Date().toISOString().slice(0, 10));
|
||||
|
||||
const investors = useMemo(() => users.filter((u) => u.role === "investor"), [users]);
|
||||
const userById = useMemo(() => new Map(users.map((u) => [u.id, u])), [users]);
|
||||
@@ -15,6 +19,17 @@ export default function CapitalAccounts() {
|
||||
const load = () => {
|
||||
api.listCapitalAccounts().then(setRows).catch((e) => setError(e.message));
|
||||
};
|
||||
|
||||
const saveExit = async (r: CapitalAccount, exitedOn: string | null) => {
|
||||
setError("");
|
||||
try {
|
||||
await api.setPartnerExited(r.entity_id, r.investor_user_id, exitedOn);
|
||||
setExitingKey(null);
|
||||
load();
|
||||
} catch (e: any) {
|
||||
setError(e.message || "Failed to update exit status");
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
api.listEntities().then(setEntities).catch(() => {});
|
||||
api.listUsers().then(setUsers).catch(() => {});
|
||||
@@ -48,13 +63,14 @@ export default function CapitalAccounts() {
|
||||
<th className="px-4 py-2 font-medium text-right">Contributions</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Distributions</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Ending balance</th>
|
||||
<th className="px-4 py-2 font-medium">Status</th>
|
||||
<th className="px-4 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.id} className="border-t border-gray-100">
|
||||
<td className="px-4 py-2 text-gray-900">
|
||||
<td className={`px-4 py-2 ${r.exited_on ? "text-gray-500" : "text-gray-900"}`}>
|
||||
{userById.get(r.investor_user_id)?.name ?? r.investor_user_id}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-600">{entityById.get(r.entity_id)?.name ?? r.entity_id}</td>
|
||||
@@ -64,6 +80,49 @@ export default function CapitalAccounts() {
|
||||
<td className="px-4 py-2 text-right text-gray-900 font-medium">
|
||||
{formatMoneyExact(r.ending_balance_cents)}
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
{r.exited_on ? (
|
||||
<span>
|
||||
<span className="text-xs font-medium uppercase text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded">
|
||||
Exited {formatDate(r.exited_on)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => saveExit(r, null)}
|
||||
className="ml-2 text-xs text-accent-600 hover:text-accent-700"
|
||||
>
|
||||
Undo
|
||||
</button>
|
||||
</span>
|
||||
) : exitingKey === `${r.entity_id}:${r.investor_user_id}` ? (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="date"
|
||||
value={exitDate}
|
||||
onChange={(e) => setExitDate(e.target.value)}
|
||||
className="px-1.5 py-0.5 border border-gray-300 rounded text-xs"
|
||||
/>
|
||||
<button
|
||||
onClick={() => exitDate && saveExit(r, exitDate)}
|
||||
className="text-xs text-accent-600 hover:text-accent-700 font-medium"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setExitingKey(null)}
|
||||
className="text-xs text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setExitingKey(`${r.entity_id}:${r.investor_user_id}`)}
|
||||
className="text-xs text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
Mark exited
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -79,7 +138,7 @@ export default function CapitalAccounts() {
|
||||
))}
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-6 text-center text-gray-400">
|
||||
<td colSpan={8} className="px-4 py-6 text-center text-gray-400">
|
||||
No statements yet.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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.33";
|
||||
export const APP_VERSION = "0.2.34";
|
||||
|
||||
Reference in New Issue
Block a user