'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Bar } from 'recharts'; import { BrainCircuit, Plus, RefreshCcw, Trash2 } from 'lucide-react'; import { AppShell } from '@/components/shell/app-shell'; import { Panel } from '@/components/ui/panel'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { StatusPill } from '@/components/ui/status-pill'; import { useAuthGuard } from '@/hooks/use-auth-guard'; import { useTaskPoller } from '@/hooks/use-task-poller'; import { deleteHolding, getLatestPortfolioInsight, getTask, getPortfolioSummary, listHoldings, queuePortfolioInsights, queuePriceRefresh, upsertHolding } from '@/lib/api'; import type { Holding, PortfolioInsight, PortfolioSummary, Task } from '@/lib/types'; import { asNumber, formatCurrency, formatPercent } from '@/lib/format'; type FormState = { ticker: string; shares: string; avgCost: string; currentPrice: string; }; const CHART_COLORS = ['#6effd8', '#5fd3ff', '#66ffa1', '#8dbbff', '#f4f88f', '#ff9c9c']; const EMPTY_SUMMARY: PortfolioSummary = { positions: 0, total_value: '0', total_gain_loss: '0', total_cost_basis: '0', avg_return_pct: '0' }; export default function PortfolioPage() { const { isPending, isAuthenticated } = useAuthGuard(); const [holdings, setHoldings] = useState([]); const [summary, setSummary] = useState(EMPTY_SUMMARY); const [latestInsight, setLatestInsight] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTask, setActiveTask] = useState(null); const [form, setForm] = useState({ ticker: '', shares: '', avgCost: '', currentPrice: '' }); const loadPortfolio = useCallback(async () => { setLoading(true); setError(null); try { const [holdingsRes, summaryRes, insightRes] = await Promise.all([ listHoldings(), getPortfolioSummary(), getLatestPortfolioInsight() ]); setHoldings(holdingsRes.holdings); setSummary(summaryRes.summary); setLatestInsight(insightRes.insight); } catch (err) { setError(err instanceof Error ? err.message : 'Could not fetch portfolio data'); } finally { setLoading(false); } }, []); useEffect(() => { if (!isPending && isAuthenticated) { void loadPortfolio(); } }, [isPending, isAuthenticated, loadPortfolio]); const polledTask = useTaskPoller({ taskId: activeTask?.id ?? null, onTerminalState: async () => { setActiveTask(null); await loadPortfolio(); } }); const liveTask = polledTask ?? activeTask; const allocationData = useMemo( () => holdings.map((holding) => ({ name: holding.ticker, value: asNumber(holding.market_value) })), [holdings] ); const performanceData = useMemo( () => holdings.map((holding) => ({ name: holding.ticker, value: asNumber(holding.gain_loss_pct) })), [holdings] ); const submitHolding = async (event: React.FormEvent) => { event.preventDefault(); try { await upsertHolding({ ticker: form.ticker.toUpperCase(), shares: Number(form.shares), avgCost: Number(form.avgCost), currentPrice: form.currentPrice ? Number(form.currentPrice) : undefined }); setForm({ ticker: '', shares: '', avgCost: '', currentPrice: '' }); await loadPortfolio(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save holding'); } }; const queueRefresh = async () => { try { const { task } = await queuePriceRefresh(); const latest = await getTask(task.id); setActiveTask(latest.task); } catch (err) { setError(err instanceof Error ? err.message : 'Unable to queue price refresh'); } }; const queueInsights = async () => { try { const { task } = await queuePortfolioInsights(); const latest = await getTask(task.id); setActiveTask(latest.task); } catch (err) { setError(err instanceof Error ? err.message : 'Unable to queue portfolio insights'); } }; if (isPending || !isAuthenticated) { return
Loading portfolio matrix...
; } return ( )} > {liveTask ? (

{liveTask.task_type}

{liveTask.error ?

{liveTask.error}

: null}
) : null} {error ? (

{error}

) : null}

{formatCurrency(summary.total_value)}

Cost basis {formatCurrency(summary.total_cost_basis)}

= 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'}`}> {formatCurrency(summary.total_gain_loss)}

Average return {formatPercent(summary.avg_return_pct)}

{summary.positions}

Active symbols in portfolio.

{loading ? (

Loading chart...

) : allocationData.length > 0 ? (
{allocationData.map((entry, index) => ( ))} formatCurrency(value)} />
) : (

No holdings yet.

)}
{loading ? (

Loading chart...

) : performanceData.length > 0 ? (
`${asNumber(value).toFixed(2)}%`} />
) : (

No performance data yet.

)}
{loading ? (

Loading holdings...

) : holdings.length === 0 ? (

No holdings added yet.

) : (
{holdings.map((holding) => ( ))}
Ticker Shares Avg Cost Price Value Gain/Loss Action
{holding.ticker} {asNumber(holding.shares).toLocaleString()} {formatCurrency(holding.avg_cost)} {holding.current_price ? formatCurrency(holding.current_price) : 'n/a'} {formatCurrency(holding.market_value)} = 0 ? 'text-[#96f5bf]' : 'text-[#ff9898]'}> {formatCurrency(holding.gain_loss)} ({formatPercent(holding.gain_loss_pct)})
)}
setForm((prev) => ({ ...prev, ticker: event.target.value.toUpperCase() }))} required />
setForm((prev) => ({ ...prev, shares: event.target.value }))} required />
setForm((prev) => ({ ...prev, avgCost: event.target.value }))} required />
setForm((prev) => ({ ...prev, currentPrice: event.target.value }))} />

Latest AI Insight

{latestInsight?.content ?? 'No insight available yet. Queue an AI brief from the header.'}

); }