Cumulative checkpoint since 0.2.26:
- 0.2.27/28: entity valuation-history table; investor gain/loss = NAV +
distributions vs paid-in
- 0.2.29: Reset Fund Partners (endpoint, Partners-tab button, CLI, action)
- 0.2.30: "Current Capital Balance" label, %-only gain/loss
- 0.2.31: Management Entities rename, Carry Vehicle type, chart
distributions-line gate
- 0.2.32: LP-facing polish pass
* Ten31 brand palette from the logo (navy/mint); orange retired
* portfolio summary card across funds; gain labeled "net of paid-in"
* whole-dollar headline figures; "History · N quarters" toggle
* documents grouped by year with a "New" badge (users.docs_seen_at)
* eNAV-created members start on default password with login enabled;
enable-investor-logins CLI + StartOS action for existing accounts
* password minimum raised to 8 chars; login help line (Portal@ten31.xyz)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
3.7 KiB
TypeScript
92 lines
3.7 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { useParams } from "react-router-dom";
|
|
import { api, type Entity, type PortalDocument, type User } from "../api";
|
|
import { categoryLabel, formatDate } from "../format";
|
|
import EntityHeader from "../components/EntityHeader";
|
|
|
|
export default function EntityDocuments() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const [entity, setEntity] = useState<Entity | null>(null);
|
|
const [docs, setDocs] = useState<PortalDocument[]>([]);
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [error, setError] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const userById = useMemo(() => new Map(users.map((u) => [u.id, u])), [users]);
|
|
|
|
const loadDocs = (eid: number) =>
|
|
api.listDocuments({ entity_id: eid }).then(setDocs).catch((e) => setError(e.message));
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
const eid = parseInt(id);
|
|
api.getEntity(eid).then(setEntity).catch((e) => setError(e.message));
|
|
api.listUsers().then(setUsers).catch(() => {});
|
|
loadDocs(eid).finally(() => setLoading(false));
|
|
}, [id]);
|
|
|
|
if (loading || !entity) return <div className="text-gray-500 text-sm">Loading…</div>;
|
|
const eid = entity.id!;
|
|
|
|
return (
|
|
<div>
|
|
<EntityHeader entity={entity} active="documents" />
|
|
|
|
{error && <p className="text-sm text-red-600 mb-3">{error}</p>}
|
|
<p className="text-sm text-gray-500 mb-3">
|
|
All documents for this fund. As admin you see everything; each investor only sees documents
|
|
shared to the fund or addressed to them. Upload from the Documents admin screen.
|
|
</p>
|
|
|
|
<div className="bg-white border border-gray-200 rounded-lg overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-gray-50 text-gray-500 text-left">
|
|
<tr>
|
|
<th className="px-4 py-2 font-medium">Title</th>
|
|
<th className="px-4 py-2 font-medium">Category</th>
|
|
<th className="px-4 py-2 font-medium">Visibility</th>
|
|
<th className="px-4 py-2 font-medium">Uploaded</th>
|
|
<th className="px-4 py-2"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{docs.map((d) => (
|
|
<tr key={d.id} className="border-t border-gray-100">
|
|
<td className="px-4 py-2 text-gray-900">{d.title}</td>
|
|
<td className="px-4 py-2 text-gray-600">{categoryLabel(d.category)}</td>
|
|
<td className="px-4 py-2 text-gray-600">
|
|
{d.investor_user_id == null
|
|
? "Shared (all investors)"
|
|
: `Private · ${userById.get(d.investor_user_id)?.name ?? d.investor_user_id}`}
|
|
</td>
|
|
<td className="px-4 py-2 text-gray-500">{formatDate(d.created_at)}</td>
|
|
<td className="px-4 py-2 text-right whitespace-nowrap">
|
|
<a href={api.downloadUrl(d.id)} className="text-accent-600 hover:text-accent-700 mr-3">
|
|
Download
|
|
</a>
|
|
<button
|
|
onClick={() => {
|
|
if (confirm(`Delete "${d.title}"?`))
|
|
api.deleteDocument(d.id).then(() => loadDocs(eid)).catch((e) => setError(e.message));
|
|
}}
|
|
className="text-gray-400 hover:text-red-600"
|
|
>
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{docs.length === 0 && (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-6 text-center text-gray-400">
|
|
No documents for this fund yet.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|