import React, { useState, useEffect, useCallback } from 'react'; import { Box, Flex, VStack, HStack, Text, Badge, Code, Spinner, Select, Table, Thead, Tbody, Tr, Th, Td, Tabs, TabList, TabPanels, Tab, TabPanel, Icon, } from '@chakra-ui/react'; import { Wallet, ArrowUpRight, ArrowDownRight, RefreshCw } from 'lucide-react'; import axios from 'axios'; const API_BASE = 'http://127.0.0.1:8088/api'; interface Holding { ticker: string; quantity: number; avg_cost: number; current_price?: number; market_value?: number; unrealized_pnl?: number; sector?: string; [key: string]: unknown; } interface Trade { id?: string; ticker: string; action: string; quantity: number; price: number; executed_at?: string; rationale?: string; stop_loss?: number | null; take_profit?: number | null; [key: string]: unknown; } interface PortfolioInfo { id: string; name?: string; cash_balance?: number; [key: string]: unknown; } interface PortfolioState { portfolio: PortfolioInfo; snapshot: Record | null; holdings: Holding[]; recent_trades: Trade[]; } interface PortfolioViewerProps { defaultPortfolioId?: string; } export const PortfolioViewer: React.FC = ({ defaultPortfolioId = 'main_portfolio' }) => { const [portfolios, setPortfolios] = useState([]); const [selectedId, setSelectedId] = useState(defaultPortfolioId); const [state, setState] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Fetch portfolio list useEffect(() => { const fetchList = async () => { try { const res = await axios.get(`${API_BASE}/portfolios/`); const list = res.data as PortfolioInfo[]; setPortfolios(list); if (list.length > 0 && !list.find((p) => p.id === selectedId)) { setSelectedId(list[0].id); } } catch { // Might fail if no DB — use fallback setPortfolios([{ id: defaultPortfolioId, name: defaultPortfolioId }]); } }; fetchList(); }, [defaultPortfolioId, selectedId]); // Fetch portfolio state when selection changes const fetchState = useCallback(async () => { if (!selectedId) return; setLoading(true); setError(null); try { const res = await axios.get(`${API_BASE}/portfolios/${selectedId}/latest`); setState(res.data as PortfolioState); } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Failed to load portfolio'; setError(msg); setState(null); } finally { setLoading(false); } }, [selectedId]); useEffect(() => { fetchState(); }, [fetchState]); return ( {/* Header */} Portfolio Viewer {/* Body */} {loading && ( )} {error && ( {error} Make sure the backend is running and the portfolio exists. )} {!loading && !error && state && ( Holdings ({state.holdings.length}) Trade History ({state.recent_trades.length}) Summary {/* Holdings */} {state.holdings.length === 0 ? ( No holdings found. ) : ( {state.holdings.map((h, i) => { const pnl = h.unrealized_pnl ?? 0; return ( ); })}
Ticker Qty Avg Cost Mkt Value P&L Sector
{h.ticker} {h.quantity} ${(h.avg_cost ?? 0).toFixed(2)} ${(h.market_value ?? 0).toFixed(2)} = 0 ? 'green.400' : 'red.400'}> = 0 ? ArrowUpRight : ArrowDownRight} boxSize={3} /> ${Math.abs(pnl).toFixed(2)} {h.sector || '—'}
)}
{/* Trade History */} {state.recent_trades.length === 0 ? ( No trades recorded yet. ) : ( {state.recent_trades.map((t, i) => ( {t.action?.toUpperCase()} {t.ticker} {t.quantity} @ ${(t.price ?? 0).toFixed(2)} {(t.stop_loss != null || t.take_profit != null) && ( {t.stop_loss != null && ( SL: ${t.stop_loss.toFixed(2)} )} {t.take_profit != null && ( TP: ${t.take_profit.toFixed(2)} )} )} {t.executed_at || '—'} {t.rationale && ( {t.rationale} )} ))} )} {/* Summary */} Portfolio ID: {state.portfolio.id} {state.portfolio.cash_balance != null && ( Cash Balance: ${state.portfolio.cash_balance.toFixed(2)} )} {state.snapshot && ( Latest Snapshot {JSON.stringify(state.snapshot, null, 2)} )}
)} {!loading && !error && !state && ( Select a portfolio to view )}
); };