Files
Ten31-Portal/frontend/src/pages/EntityDocuments.tsx
T
Jonathan KirkwoodandClaude Opus 4.8 4215d4478f 0.2.27-0.2.32: LP portal polish, brand palette, default investor logins
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>
2026-07-03 08:40:40 -05:00

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>
);
}