'use client'; import { useQueryClient } from '@tanstack/react-query'; import Link from 'next/link'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Bar } from 'recharts'; import { BrainCircuit, Plus, RefreshCcw, SquarePen, 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 { useAuthGuard } from '@/hooks/use-auth-guard'; import { useLinkPrefetch } from '@/hooks/use-link-prefetch'; import { deleteHolding, queuePortfolioInsights, queuePriceRefresh, updateHolding, upsertHolding } from '@/lib/api'; import type { Holding, PortfolioInsight, PortfolioSummary } from '@/lib/types'; import { asNumber, formatCurrency, formatPercent } from '@/lib/format'; import { queryKeys } from '@/lib/query/keys'; import { holdingsQueryOptions, latestPortfolioInsightQueryOptions, portfolioSummaryQueryOptions } from '@/lib/query/options'; type FormState = { ticker: string; companyName: string; shares: string; avgCost: string; currentPrice: string; }; const CHART_COLORS = ['#6effd8', '#5fd3ff', '#66ffa1', '#8dbbff', '#f4f88f', '#ff9c9c']; const CHART_TEXT = '#e8fff8'; const CHART_MUTED = '#b4ced9'; const CHART_GRID = 'rgba(126, 217, 255, 0.24)'; const CHART_TOOLTIP_BG = 'rgba(6, 17, 24, 0.95)'; const CHART_TOOLTIP_BORDER = 'rgba(123, 255, 217, 0.45)'; 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 queryClient = useQueryClient(); const { prefetchResearchTicker } = useLinkPrefetch(); 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 [editingHoldingId, setEditingHoldingId] = useState(null); const [form, setForm] = useState({ ticker: '', companyName: '', shares: '', avgCost: '', currentPrice: '' }); const loadPortfolio = useCallback(async () => { const holdingsOptions = holdingsQueryOptions(); const summaryOptions = portfolioSummaryQueryOptions(); const insightOptions = latestPortfolioInsightQueryOptions(); if (!queryClient.getQueryData(summaryOptions.queryKey)) { setLoading(true); } setError(null); try { const [holdingsRes, summaryRes, insightRes] = await Promise.all([ queryClient.fetchQuery(holdingsOptions), queryClient.fetchQuery(summaryOptions), queryClient.fetchQuery(insightOptions) ]); 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); } }, [queryClient]); useEffect(() => { if (!isPending && isAuthenticated) { void loadPortfolio(); } }, [isPending, isAuthenticated, loadPortfolio]); 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 resetHoldingForm = useCallback(() => { setEditingHoldingId(null); setForm({ ticker: '', companyName: '', shares: '', avgCost: '', currentPrice: '' }); }, []); const submitHolding = async (event: React.FormEvent) => { event.preventDefault(); try { if (editingHoldingId === null) { await upsertHolding({ ticker: form.ticker.toUpperCase(), companyName: form.companyName.trim() || undefined, shares: Number(form.shares), avgCost: Number(form.avgCost), currentPrice: form.currentPrice ? Number(form.currentPrice) : undefined }); } else { await updateHolding(editingHoldingId, { companyName: form.companyName.trim() || undefined, shares: Number(form.shares), avgCost: Number(form.avgCost), currentPrice: form.currentPrice ? Number(form.currentPrice) : undefined }); } resetHoldingForm(); void queryClient.invalidateQueries({ queryKey: queryKeys.holdings() }); void queryClient.invalidateQueries({ queryKey: queryKeys.portfolioSummary() }); await loadPortfolio(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save holding'); } }; const queueRefresh = async () => { try { await queuePriceRefresh(); void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) }); void queryClient.invalidateQueries({ queryKey: queryKeys.portfolioSummary() }); await loadPortfolio(); } catch (err) { setError(err instanceof Error ? err.message : 'Unable to queue price refresh'); } }; const queueInsights = async () => { try { await queuePortfolioInsights(); void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) }); void queryClient.invalidateQueries({ queryKey: queryKeys.latestPortfolioInsight() }); await loadPortfolio(); } catch (err) { setError(err instanceof Error ? err.message : 'Unable to queue portfolio insights'); } }; if (isPending || !isAuthenticated) { return
Loading portfolio matrix...
; } return ( )} > {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)} contentStyle={{ backgroundColor: CHART_TOOLTIP_BG, border: `1px solid ${CHART_TOOLTIP_BORDER}`, borderRadius: '0.75rem' }} labelStyle={{ color: CHART_TEXT }} itemStyle={{ color: CHART_TEXT }} /> {value}} />
) : (

No holdings yet.

)}
{loading ? (

Loading chart...

) : performanceData.length > 0 ? (
`${asNumber(value).toFixed(2)}%`} contentStyle={{ backgroundColor: CHART_TOOLTIP_BG, border: `1px solid ${CHART_TOOLTIP_BORDER}`, borderRadius: '0.75rem' }} labelStyle={{ color: CHART_TEXT }} itemStyle={{ color: CHART_TEXT }} cursor={{ fill: 'rgba(104, 255, 213, 0.08)' }} />
) : (

No performance data yet.

)}
{loading ? (

Loading holdings...

) : holdings.length === 0 ? (

No holdings added yet.

) : (
{holdings.map((holding) => (

{holding.ticker}

{holding.company_name ?? 'Company name unavailable'}

= 0 ? 'text-[#96f5bf]' : 'text-[#ff9898]'}`}> {formatCurrency(holding.gain_loss)}

Shares
{asNumber(holding.shares).toLocaleString()}
Avg Cost
{formatCurrency(holding.avg_cost)}
Price
{holding.current_price ? formatCurrency(holding.current_price) : 'n/a'}
Value
{formatCurrency(holding.market_value)}

= 0 ? 'text-[#96f5bf]' : 'text-[#ff9898]'}`}> Return {formatPercent(holding.gain_loss_pct)}

prefetchResearchTicker(holding.ticker)} onFocus={() => prefetchResearchTicker(holding.ticker)} className="inline-flex items-center rounded-md border border-[color:var(--line-weak)] px-2 py-1 text-xs text-[color:var(--accent)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--accent-strong)]" > Analysis prefetchResearchTicker(holding.ticker)} onFocus={() => prefetchResearchTicker(holding.ticker)} className="inline-flex items-center rounded-md border border-[color:var(--line-weak)] px-2 py-1 text-xs text-[color:var(--accent)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--accent-strong)]" > Financials prefetchResearchTicker(holding.ticker)} onFocus={() => prefetchResearchTicker(holding.ticker)} className="inline-flex items-center rounded-md border border-[color:var(--line-weak)] px-2 py-1 text-xs text-[color:var(--accent)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--accent-strong)]" > Filings
))}
{holdings.map((holding) => ( ))}
Ticker Company Shares Avg Cost Price Value Gain/Loss Research Action
{holding.ticker} {holding.company_name ?? 'n/a'} {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)})
prefetchResearchTicker(holding.ticker)} onFocus={() => prefetchResearchTicker(holding.ticker)} className="text-xs text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Analysis prefetchResearchTicker(holding.ticker)} onFocus={() => prefetchResearchTicker(holding.ticker)} className="text-xs text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Financials prefetchResearchTicker(holding.ticker)} onFocus={() => prefetchResearchTicker(holding.ticker)} className="text-xs text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Filings
)}
setForm((prev) => ({ ...prev, ticker: event.target.value.toUpperCase() }))} required />
setForm((prev) => ({ ...prev, companyName: event.target.value }))} placeholder="Resolved from coverage or filings if left blank" />
setForm((prev) => ({ ...prev, shares: event.target.value }))} required />
setForm((prev) => ({ ...prev, avgCost: event.target.value }))} required />
setForm((prev) => ({ ...prev, currentPrice: event.target.value }))} />
{editingHoldingId !== null ? ( ) : null}

Latest AI Insight

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

); }