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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
69f12b0519
commit
4215d4478f
@@ -115,7 +115,7 @@ export default function AccessGrid() {
|
||||
disabled={loading}
|
||||
className={`w-full h-10 flex items-center justify-center transition-colors ${
|
||||
on
|
||||
? "bg-orange-50 text-orange-600 hover:bg-orange-100"
|
||||
? "bg-accent-50 text-accent-600 hover:bg-accent-100"
|
||||
: "text-gray-300 hover:bg-gray-100"
|
||||
} ${loading ? "opacity-50" : ""}`}
|
||||
title={on ? "Click to revoke" : "Click to grant"}
|
||||
|
||||
@@ -186,7 +186,7 @@ function EntryForm({
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={busy || entityId === "" || investorId === "" || !asOf}
|
||||
className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50"
|
||||
className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50"
|
||||
>
|
||||
{busy ? "Saving…" : "Save statement"}
|
||||
</button>
|
||||
@@ -219,4 +219,4 @@ function DollarField({
|
||||
}
|
||||
|
||||
const inputCls =
|
||||
"w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent";
|
||||
"w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent";
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function Documents() {
|
||||
<td className="px-4 py-2 text-right whitespace-nowrap">
|
||||
<a
|
||||
href={api.downloadUrl(d.id)}
|
||||
className="text-orange-600 hover:text-orange-700 mr-3"
|
||||
className="text-accent-600 hover:text-accent-700 mr-3"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
@@ -242,7 +242,7 @@ function UploadForm({
|
||||
{targetLabel ? (
|
||||
<p className="text-xs text-gray-500">
|
||||
Uploading to <span className="font-medium text-gray-700">{entityName}</span> ·{" "}
|
||||
<span className={investorId === "" ? "text-gray-700" : "text-orange-600 font-medium"}>
|
||||
<span className={investorId === "" ? "text-gray-700" : "text-accent-600 font-medium"}>
|
||||
{targetLabel}
|
||||
</span>
|
||||
</p>
|
||||
@@ -252,7 +252,7 @@ function UploadForm({
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={busy || entityId === "" || !file}
|
||||
className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50"
|
||||
className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50"
|
||||
>
|
||||
{busy ? "Uploading…" : "Upload"}
|
||||
</button>
|
||||
@@ -262,4 +262,4 @@ function UploadForm({
|
||||
}
|
||||
|
||||
const inputCls =
|
||||
"w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent";
|
||||
"w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent";
|
||||
|
||||
@@ -21,6 +21,7 @@ const TYPE_LABELS: Record<string, string> = {
|
||||
spv: "SPV",
|
||||
gp: "GP",
|
||||
mgmt_co: "Mgmt Co",
|
||||
carry: "Carry Vehicle",
|
||||
};
|
||||
|
||||
export default function EntitiesList() {
|
||||
@@ -62,7 +63,9 @@ export default function EntitiesList() {
|
||||
}
|
||||
|
||||
const funds = entities.filter((e) => e.type === "fund" || e.type === "spv");
|
||||
const gps = entities.filter((e) => e.type === "gp" || e.type === "mgmt_co");
|
||||
const gps = entities.filter(
|
||||
(e) => e.type === "gp" || e.type === "mgmt_co" || e.type === "carry",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -71,7 +74,7 @@ export default function EntitiesList() {
|
||||
{isWriter && (
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="px-4 py-2 bg-gray-900 text-white text-sm rounded hover:bg-gray-800"
|
||||
className="px-4 py-2 bg-brand-900 text-white text-sm rounded hover:bg-brand-800"
|
||||
>
|
||||
Add Entity
|
||||
</button>
|
||||
@@ -89,7 +92,7 @@ export default function EntitiesList() {
|
||||
)}
|
||||
|
||||
<EntityTable title="Funds and SPVs" rows={funds} />
|
||||
<EntityTable title="GP Entities and Management Companies" rows={gps} />
|
||||
<EntityTable title="Management Entities" rows={gps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -155,7 +158,7 @@ function AddEntityForm({
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent"
|
||||
placeholder="Low Time Preference Fund I, LLC"
|
||||
required
|
||||
/>
|
||||
@@ -171,6 +174,7 @@ function AddEntityForm({
|
||||
<option value="spv">SPV</option>
|
||||
<option value="gp">GP</option>
|
||||
<option value="mgmt_co">Mgmt Co</option>
|
||||
<option value="carry">Carry Vehicle</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -202,7 +206,7 @@ function AddEntityForm({
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50"
|
||||
className="px-4 py-2 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Creating..." : "Create"}
|
||||
</button>
|
||||
@@ -261,7 +265,7 @@ function EntityTable({ title, rows }: { title: string; rows: EntityRow[] }) {
|
||||
return (
|
||||
<th className={`px-4 py-3 font-medium text-gray-600 select-none ${a}`}>
|
||||
<button onClick={() => sortBy(k)} className={`w-full hover:text-gray-900 ${a}`}>
|
||||
{label}<span className="text-orange-500">{arrow(k)}</span>
|
||||
{label}<span className="text-accent-500">{arrow(k)}</span>
|
||||
</button>
|
||||
</th>
|
||||
);
|
||||
@@ -345,7 +349,7 @@ function EntityTable({ title, rows }: { title: string; rows: EntityRow[] }) {
|
||||
<td className="px-4 py-3">
|
||||
<Link
|
||||
to={`/entities/${row.id}`}
|
||||
className="text-gray-900 font-medium hover:text-orange-600"
|
||||
className="text-gray-900 font-medium hover:text-accent-600"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function EntityAssets() {
|
||||
<p>This entity isn't linked to an investor account yet, so there are no balances to show.</p>
|
||||
<p className="mt-2 text-gray-500">
|
||||
Link it on the{" "}
|
||||
<Link to={`/entities/${entityId}`} className="text-orange-600 hover:text-orange-700">
|
||||
<Link to={`/entities/${entityId}`} className="text-accent-600 hover:text-accent-700">
|
||||
Overview tab → Edit entity
|
||||
</Link>{" "}
|
||||
(choose its investor account). Its capital-account balance in each fund will then
|
||||
@@ -110,7 +110,7 @@ export default function EntityAssets() {
|
||||
{rows.map((r) => (
|
||||
<tr key={r.fundId} className="border-t border-gray-100">
|
||||
<td className="px-4 py-2 text-gray-900">
|
||||
<Link to={`/entities/${r.fundId}`} className="hover:text-orange-600">
|
||||
<Link to={`/entities/${r.fundId}`} className="hover:text-accent-600">
|
||||
{fundName(r.fundId)}
|
||||
</Link>
|
||||
</td>
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function EntityDocuments() {
|
||||
</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-orange-600 hover:text-orange-700 mr-3">
|
||||
<a href={api.downloadUrl(d.id)} className="text-accent-600 hover:text-accent-700 mr-3">
|
||||
Download
|
||||
</a>
|
||||
<button
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function EntityOverview() {
|
||||
const [totalInvested, setTotalInvested] = useState(0);
|
||||
const [lastValue, setLastValue] = useState(0);
|
||||
const [latestRound, setLatestRound] = useState<ValuationRound | null>(null);
|
||||
const [rounds, setRounds] = useState<ValuationRound[]>([]);
|
||||
const [assetBalances, setAssetBalances] = useState<AssetBalances | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editing, setEditing] = useState(false);
|
||||
@@ -49,7 +50,10 @@ export default function EntityOverview() {
|
||||
const ent = await api.getEntity(entityId);
|
||||
setEntity(ent);
|
||||
|
||||
if ((ent.type === "gp" || ent.type === "mgmt_co") && ent.linked_user_id != null) {
|
||||
if (
|
||||
(ent.type === "gp" || ent.type === "mgmt_co" || ent.type === "carry") &&
|
||||
ent.linked_user_id != null
|
||||
) {
|
||||
api.entityAssetBalances(entityId).then(setAssetBalances).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -66,6 +70,9 @@ export default function EntityOverview() {
|
||||
setTotalInvested(invested);
|
||||
|
||||
const rounds = await api.listRounds(entityId);
|
||||
// Newest quarter first, so the history table reads top-down like a statement run.
|
||||
const sorted = [...rounds].sort((a, b) => b.quarter_end.localeCompare(a.quarter_end));
|
||||
setRounds(sorted);
|
||||
const approved = rounds.filter((r) => r.status === "approved");
|
||||
if (approved.length > 0) {
|
||||
setLatestRound(approved[0]);
|
||||
@@ -141,7 +148,7 @@ export default function EntityOverview() {
|
||||
{assetBalances.linked_name ? ` · ${assetBalances.linked_name}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<Link to={`/entities/${entity.id}/assets`} className="text-sm text-orange-600 hover:text-orange-700">
|
||||
<Link to={`/entities/${entity.id}/assets`} className="text-sm text-accent-600 hover:text-accent-700">
|
||||
View by fund →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -165,6 +172,47 @@ export default function EntityOverview() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Valuation history — every quarter uploaded/signed for this entity */}
|
||||
{rounds.length > 0 && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg overflow-x-auto mt-6">
|
||||
<div className="px-4 py-3 border-b border-gray-100">
|
||||
<h3 className="text-sm font-medium text-gray-700">Valuation history</h3>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
{rounds.length} quarter{rounds.length === 1 ? "" : "s"} on record
|
||||
</p>
|
||||
</div>
|
||||
<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">Quarter</th>
|
||||
<th className="px-4 py-2 font-medium text-right">NAV</th>
|
||||
<th className="px-4 py-2 font-medium">Status</th>
|
||||
<th className="px-4 py-2 font-medium">Signed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rounds.map((r) => {
|
||||
const nav = r.valuations.reduce((s, v) => s + v.value_cents, 0);
|
||||
return (
|
||||
<tr key={r.id} className="border-t border-gray-100">
|
||||
<td className="px-4 py-2 text-gray-900">{formatQuarter(r.quarter_end)}</td>
|
||||
<td className="px-4 py-2 text-right text-gray-900 font-medium">{formatMoney(nav)}</td>
|
||||
<td className="px-4 py-2">
|
||||
<span className={`inline-block px-2 py-0.5 text-xs rounded ${STATUS_COLORS[r.status]}`}>
|
||||
{r.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500">
|
||||
{r.status === "approved" && r.approved_at ? formatDate(r.approved_at) : "—"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -191,9 +239,9 @@ function EditEntityForm({
|
||||
const [saving, setSaving] = useState(false);
|
||||
const inputCls = "w-full px-3 py-2 border border-gray-300 rounded text-sm";
|
||||
|
||||
// A GP / management company can be linked to its own investor account, so its Assets view
|
||||
// shows its real per-fund balances.
|
||||
const isGp = type === "gp" || type === "mgmt_co";
|
||||
// A GP / management company / carry vehicle can be linked to its own investor account, so its
|
||||
// Assets view shows its real per-fund balances.
|
||||
const isGp = type === "gp" || type === "mgmt_co" || type === "carry";
|
||||
useEffect(() => {
|
||||
if (!isGp) return;
|
||||
api.listUsers().then((us) => setInvestors(us.filter((u) => u.role === "investor"))).catch(() => {});
|
||||
@@ -243,6 +291,7 @@ function EditEntityForm({
|
||||
<option value="spv">SPV</option>
|
||||
<option value="gp">GP</option>
|
||||
<option value="mgmt_co">Mgmt Co</option>
|
||||
<option value="carry">Carry Vehicle</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -282,7 +331,7 @@ function EditEntityForm({
|
||||
)}
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
<div className="flex gap-2 pt-1">
|
||||
<button type="submit" disabled={saving} className="px-4 py-2 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50">
|
||||
<button type="submit" disabled={saving} className="px-4 py-2 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50">
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
<button type="button" onClick={onClose} className="px-4 py-2 border border-gray-300 text-sm rounded hover:bg-gray-50">
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { api, type Entity, type Partner } from "../api";
|
||||
import { api, canEditRound, type Entity, type Partner } from "../api";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { formatDate, formatMoneyExact } from "../format";
|
||||
import EntityHeader from "../components/EntityHeader";
|
||||
|
||||
export default function EntityPartners() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { user } = useAuth();
|
||||
const [entity, setEntity] = useState<Entity | null>(null);
|
||||
const [partners, setPartners] = useState<Partner[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [notice, setNotice] = useState("");
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const isWriter = !!user && canEditRound(user.role);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
const eid = parseInt(id);
|
||||
@@ -23,6 +29,34 @@ export default function EntityPartners() {
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
async function clearAllPartners() {
|
||||
if (!entity) return;
|
||||
if (
|
||||
!confirm(
|
||||
`Remove ALL ${partners.length} partner(s) from ${entity.name}?\n\n` +
|
||||
"This deletes this fund's capital-account statements and the investors' access to " +
|
||||
"it. The investor accounts themselves are kept (they may belong to other funds), " +
|
||||
"and holdings/NAV are not affected. Re-import the correct roster afterward.",
|
||||
)
|
||||
)
|
||||
return;
|
||||
setClearing(true);
|
||||
setError("");
|
||||
setNotice("");
|
||||
try {
|
||||
const res = await api.clearPartners(entity.id);
|
||||
const fresh = await api.listPartners(entity.id);
|
||||
setPartners(fresh);
|
||||
setNotice(
|
||||
`Cleared: removed ${res.statements} capital statement(s) and ${res.access_grants} access grant(s).`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to clear partners");
|
||||
} finally {
|
||||
setClearing(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading || !entity) return <div className="text-gray-500 text-sm">Loading…</div>;
|
||||
|
||||
const totalCommitted = partners.reduce((s, p) => s + (p.latest_commitment_cents || 0), 0);
|
||||
@@ -33,16 +67,28 @@ export default function EntityPartners() {
|
||||
<EntityHeader entity={entity} active="partners" />
|
||||
|
||||
{error && <p className="text-sm text-red-600 mb-3">{error}</p>}
|
||||
{notice && <p className="text-sm text-green-600 mb-3">{notice}</p>}
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm text-gray-500">
|
||||
{partners.length} member{partners.length === 1 ? "" : "s"} with access to this fund.
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
Total committed: <span className="font-medium">{formatMoneyExact(totalCommitted)}</span>
|
||||
<span className="mx-2 text-gray-300">·</span>
|
||||
Total capital: <span className="font-medium">{formatMoneyExact(totalCapital)}</span>
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-sm text-gray-700">
|
||||
Total committed: <span className="font-medium">{formatMoneyExact(totalCommitted)}</span>
|
||||
<span className="mx-2 text-gray-300">·</span>
|
||||
Total capital: <span className="font-medium">{formatMoneyExact(totalCapital)}</span>
|
||||
</p>
|
||||
{isWriter && partners.length > 0 && (
|
||||
<button
|
||||
onClick={clearAllPartners}
|
||||
disabled={clearing}
|
||||
className="px-3 py-1.5 border border-red-300 text-red-600 text-sm rounded hover:bg-red-50 disabled:opacity-50"
|
||||
>
|
||||
{clearing ? "Clearing…" : "Clear all partners"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-lg overflow-x-auto">
|
||||
|
||||
@@ -230,7 +230,7 @@ export default function Import() {
|
||||
<li>{result.members} members ({result.created} newly created)</li>
|
||||
{result.note && <li className="text-amber-700">{result.note}</li>}
|
||||
</ul>
|
||||
<button onClick={() => setResult(null)} className="mt-3 px-3 py-1.5 bg-gray-900 text-white rounded hover:bg-gray-800">
|
||||
<button onClick={() => setResult(null)} className="mt-3 px-3 py-1.5 bg-brand-900 text-white rounded hover:bg-brand-800">
|
||||
Import another
|
||||
</button>
|
||||
</div>
|
||||
@@ -240,11 +240,11 @@ export default function Import() {
|
||||
<label className="block text-sm text-gray-700 mb-2">Fund</label>
|
||||
<div className="flex gap-4 mb-2">
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
|
||||
<input type="radio" checked={entityMode === "existing"} onChange={() => { setEntityMode("existing"); reset(); }} className="text-orange-500" />
|
||||
<input type="radio" checked={entityMode === "existing"} onChange={() => { setEntityMode("existing"); reset(); }} className="text-accent-500" />
|
||||
Use existing fund
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
|
||||
<input type="radio" checked={entityMode === "from_file"} onChange={() => { setEntityMode("from_file"); reset(); }} className="text-orange-500" />
|
||||
<input type="radio" checked={entityMode === "from_file"} onChange={() => { setEntityMode("from_file"); reset(); }} className="text-accent-500" />
|
||||
Create from file
|
||||
</label>
|
||||
</div>
|
||||
@@ -260,6 +260,7 @@ export default function Import() {
|
||||
<option value="spv">SPV</option>
|
||||
<option value="gp">GP</option>
|
||||
<option value="mgmt_co">Mgmt Co</option>
|
||||
<option value="carry">Carry Vehicle</option>
|
||||
</select>
|
||||
<input className={inputCls} placeholder="Vintage year" value={vintageYear} onChange={(e) => setVintageYear(e.target.value)} />
|
||||
</div>
|
||||
@@ -278,7 +279,7 @@ export default function Import() {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button onClick={readEnav} disabled={busy || !file} className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50">
|
||||
<button onClick={readEnav} disabled={busy || !file} className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50">
|
||||
{busy ? "Reading…" : "Read eNAV"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -373,7 +374,7 @@ export default function Import() {
|
||||
{decisions.filter((d) => d.action === "match").length} matched ·{" "}
|
||||
{decisions.filter((d) => d.action === "skip").length} skipped
|
||||
</span>
|
||||
<button onClick={handleConfirmClick} disabled={busy || !asOf} className="px-4 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50">
|
||||
<button onClick={handleConfirmClick} disabled={busy || !asOf} className="px-4 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50">
|
||||
{busy ? (step || "Importing…") : "Confirm import"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -491,7 +492,7 @@ function BatchBackfill({ entities }: { entities: Entity[] }) {
|
||||
<button
|
||||
onClick={run}
|
||||
disabled={busy || !entityId || files.length === 0}
|
||||
className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50"
|
||||
className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50"
|
||||
>
|
||||
{busy ? "Loading history…" : "Load history"}
|
||||
</button>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function Login() {
|
||||
autoComplete="username"
|
||||
value={handle}
|
||||
onChange={(e) => setHandle(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -49,11 +49,17 @@ export default function Login() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-2 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50"
|
||||
className="w-full py-2 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Signing in..." : "Sign in"}
|
||||
</button>
|
||||
</form>
|
||||
<p className="mt-5 text-xs text-gray-400 text-center">
|
||||
Trouble signing in?{" "}
|
||||
<a href="mailto:Portal@ten31.xyz" className="text-accent-600 hover:text-accent-700">
|
||||
Contact Portal@ten31.xyz
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function Users() {
|
||||
<h2 className="text-xl font-semibold text-gray-900">Users</h2>
|
||||
<button
|
||||
onClick={() => setCreating(true)}
|
||||
className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800"
|
||||
className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800"
|
||||
>
|
||||
New user
|
||||
</button>
|
||||
@@ -94,7 +94,7 @@ export default function Users() {
|
||||
onClick={() =>
|
||||
api.getUser(u.id).then(setEditing).catch((e) => setError(e.message))
|
||||
}
|
||||
className="text-orange-600 hover:text-orange-700"
|
||||
className="text-accent-600 hover:text-accent-700"
|
||||
>
|
||||
Manage
|
||||
</button>
|
||||
@@ -254,7 +254,7 @@ function CreateUserModal({
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={busy || !name || !username || !password}
|
||||
className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50"
|
||||
className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
@@ -387,7 +387,7 @@ function EditUserModal({
|
||||
<button
|
||||
onClick={save}
|
||||
disabled={busy}
|
||||
className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800 disabled:opacity-50"
|
||||
className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800 disabled:opacity-50"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -398,7 +398,7 @@ function EditUserModal({
|
||||
}
|
||||
|
||||
const inputCls =
|
||||
"w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent";
|
||||
"w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent";
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -187,7 +187,7 @@ export default function ValuationWorkflow() {
|
||||
<Link to={`/entities/${entity.id}/investments`} className="pb-2 text-sm text-gray-500 hover:text-gray-800">
|
||||
Investments
|
||||
</Link>
|
||||
<span className="pb-2 border-b-2 border-orange-500 text-sm font-medium text-orange-600">
|
||||
<span className="pb-2 border-b-2 border-accent-500 text-sm font-medium text-accent-600">
|
||||
Valuation
|
||||
</span>
|
||||
</div>
|
||||
@@ -214,7 +214,7 @@ export default function ValuationWorkflow() {
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreateRound}
|
||||
className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800"
|
||||
className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
@@ -228,7 +228,7 @@ export default function ValuationWorkflow() {
|
||||
<button
|
||||
onClick={() => selectRound(r)}
|
||||
className={`w-full text-left px-3 py-2 rounded text-sm ${
|
||||
selectedRound?.id === r.id ? "bg-orange-50 text-orange-700" : "hover:bg-gray-50 text-gray-700"
|
||||
selectedRound?.id === r.id ? "bg-accent-50 text-accent-700" : "hover:bg-gray-50 text-gray-700"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">{formatQuarter(r.quarter_end)}</div>
|
||||
@@ -271,7 +271,7 @@ export default function ValuationWorkflow() {
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded hover:bg-gray-800"
|
||||
className="px-3 py-1.5 bg-brand-900 text-white text-sm rounded hover:bg-brand-800"
|
||||
>
|
||||
Submit for Review
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user