0.2.39: bitcoin-denominated view, first-login flow, unfunded + tax center
- BTC prices: btc_prices table, CSV upload on Import page (auto-detected date/close columns, upsert by date), entities.close_date as the BTC entry mark; statements carry btc_price_cents (as-of) + btc_close_price_cents. LP capital blocks show paid-in vs current value in bitcoin terms. - First login: accounts on the shared default password are flagged (must_change_password) and blocked behind a full-screen password change; external accounts then get a one-time welcome tour with a 2FA offer (users.onboarded_at). - LP portal: Unfunded (callable commitment) metric; Tax documents center aggregating K-1/tax docs across funds, grouped by year. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0822eca887
commit
eac3262f29
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import {
|
||||
api, isAdmin,
|
||||
type Entity, type EntityType, type CapitalImportPreview, type BatchCapitalImportResult,
|
||||
type BtcPricesStatus,
|
||||
} from "../api";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { formatDate, formatMoneyExact } from "../format";
|
||||
@@ -383,6 +384,8 @@ export default function Import() {
|
||||
|
||||
{!memberPv && !result && <BatchBackfill entities={entities} />}
|
||||
|
||||
{!memberPv && !result && <BtcPrices />}
|
||||
|
||||
{confirmReplace && (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
||||
@@ -542,3 +545,78 @@ function BatchBackfill({ entities }: { entities: Entity[] }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// BTC/USD price history for the bitcoin-denominated LP view: upload a CSV (date + close
|
||||
// price); statements are valued at the newest price on or before their date, and paid-in
|
||||
// at the price on each fund's close date (set on the fund's overview page).
|
||||
function BtcPrices() {
|
||||
const [status, setStatus] = useState<BtcPricesStatus | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [msg, setMsg] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
api.btcPricesStatus().then(setStatus).catch(() => {});
|
||||
}, []);
|
||||
|
||||
async function run() {
|
||||
if (!file) return;
|
||||
setError("");
|
||||
setMsg("");
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await api.importBtcPrices(file);
|
||||
setStatus(r);
|
||||
setMsg(
|
||||
`Loaded ${r.imported} price${r.imported === 1 ? "" : "s"}` +
|
||||
(r.skipped_rows ? ` (${r.skipped_rows} unreadable row${r.skipped_rows === 1 ? "" : "s"} skipped)` : "") +
|
||||
".",
|
||||
);
|
||||
setFile(null);
|
||||
} catch (e: any) {
|
||||
setError(e.message || "Price import failed");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-10 border-t border-gray-200 pt-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-1">Bitcoin prices</h2>
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
Upload a CSV of BTC/USD prices (a date column and a close/price column). Investors
|
||||
then see their capital in bitcoin terms; paid-in is valued at the price on each
|
||||
fund's close date — set that on the fund's overview page.
|
||||
</p>
|
||||
{status && status.count > 0 && (
|
||||
<p className="text-xs text-gray-400 mb-3">
|
||||
{status.count} prices on file
|
||||
{status.first_date && status.last_date && (
|
||||
<> · {formatDate(status.first_date)} → {formatDate(status.last_date)}</>
|
||||
)}
|
||||
{status.latest_price_cents != null && (
|
||||
<> · latest ${(status.latest_price_cents / 100).toLocaleString()}</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv"
|
||||
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
|
||||
className="text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={run}
|
||||
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 ? "Loading…" : "Load prices"}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-600 mt-2">{error}</p>}
|
||||
{msg && <p className="text-sm text-green-700 mt-2">{msg}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user