"use client"; import { useCallback, useEffect, useState } from "react"; import { api, type PortfolioHolding } from "@/lib/trpc"; import { UI_STRINGS } from "@/lib/strings"; import { Card, CardHeader, Button, Input } from "@/components/ui"; export interface PortfolioPanelProps { onChange?: (holdings: PortfolioHolding[]) => void; bare?: boolean; } export function PortfolioPanel({ onChange, bare = false }: PortfolioPanelProps) { const [holdings, setHoldings] = useState([]); const [draftSymbol, setDraftSymbol] = useState(""); const [draftShares, setDraftShares] = useState(""); const [draftCost, setDraftCost] = useState(""); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [editing, setEditing] = useState(null); const [editShares, setEditShares] = useState(""); const [editCost, setEditCost] = useState(""); const load = useCallback(async (): Promise => { try { const next = await api.portfolio.holdings(); setHoldings(next); setError(null); return next; } catch (e) { setError(e instanceof Error ? e.message : "Could not load holdings."); return null; } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const notifyChange = (next: PortfolioHolding[]) => { onChange?.(next); }; const add = async () => { const symbol = draftSymbol.trim().toUpperCase(); const shares = parseFloat(draftShares); const avgCost = parseFloat(draftCost); if (!symbol || !Number.isFinite(shares) || shares <= 0 || !Number.isFinite(avgCost) || avgCost < 0) { setError("Symbol, positive share count, and average cost are required."); return; } setBusy(true); setError(null); try { await api.portfolio.addHolding(symbol, shares, avgCost); setDraftSymbol(""); setDraftShares(""); setDraftCost(""); const next = await load(); if (next) notifyChange(next); } catch (e) { setError(e instanceof Error ? e.message : "Could not record holding."); } finally { setBusy(false); } }; const remove = async (symbol: string) => { setBusy(true); setError(null); try { await api.portfolio.removeHolding(symbol); const next = await load(); if (next) notifyChange(next); } catch (e) { setError(e instanceof Error ? e.message : "Could not remove holding."); } finally { setBusy(false); } }; const startEdit = (h: PortfolioHolding) => { setEditing(h.symbol); setEditShares(String(h.shares)); setEditCost(String(h.avg_cost)); setError(null); }; const cancelEdit = () => { setEditing(null); setEditShares(""); setEditCost(""); }; const saveEdit = async (symbol: string) => { const shares = parseFloat(editShares); const avgCost = parseFloat(editCost); if (!Number.isFinite(shares) || shares <= 0 || !Number.isFinite(avgCost) || avgCost < 0) { setError("Shares must be positive and avg cost must be 0 or more."); return; } setBusy(true); setError(null); try { await api.portfolio.updateHolding(symbol, { shares, avgCost }); setEditing(null); const next = await load(); if (next) notifyChange(next); } catch (e) { setError(e instanceof Error ? e.message : "Could not update holding."); } finally { setBusy(false); } }; const totalValue = holdings.reduce((sum, h) => sum + h.shares * h.avg_cost, 0); const body = ( <> {UI_STRINGS.portfolioCaption ? (

{UI_STRINGS.portfolioCaption}

) : null}
{error &&

{error}

} {loading ? (

Loading holdings…

) : holdings.length === 0 ? (

{UI_STRINGS.emptyPortfolio}

) : (
Symbol Shares Avg cost Cost basis
{holdings.map((h) => ( editing === h.symbol ? (
{h.symbol} setEditShares(e.target.value)} type="number" className="text-right" disabled={busy} /> setEditCost(e.target.value)} type="number" step="0.01" className="text-right" disabled={busy} /> ${(parseFloat(editShares || "0") * parseFloat(editCost || "0")).toFixed(0)}
) : (
startEdit(h)} > {h.symbol} {h.shares} ${h.avg_cost.toFixed(2)} ${(h.shares * h.avg_cost).toFixed(0)}
) ))}
Cost-basis total ${totalValue.toFixed(0)}
)} ); if (bare) return
{body}
; return ( {body} ); }