Files
Neon-Desk/app/financials/page.tsx
2026-03-06 14:40:43 -05:00

916 lines
34 KiB
TypeScript

'use client';
import { useQueryClient } from '@tanstack/react-query';
import Link from 'next/link';
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import { format } from 'date-fns';
import { useSearchParams } from 'next/navigation';
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Line,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import { AlertTriangle, ChartNoAxesCombined, ChevronDown, Download, RefreshCcw, Search } from 'lucide-react';
import { AppShell } from '@/components/shell/app-shell';
import { MetricCard } from '@/components/dashboard/metric-card';
import {
FinancialControlBar,
type FinancialControlAction,
type FinancialControlSection
} from '@/components/financials/control-bar';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Panel } from '@/components/ui/panel';
import { useAuthGuard } from '@/hooks/use-auth-guard';
import { useLinkPrefetch } from '@/hooks/use-link-prefetch';
import { queueFilingSync } from '@/lib/api';
import {
formatCurrencyByScale,
formatPercent,
type NumberScaleUnit
} from '@/lib/format';
import { queryKeys } from '@/lib/query/keys';
import { companyFinancialStatementsQueryOptions } from '@/lib/query/options';
import type {
CompanyFinancialStatementsResponse,
DimensionBreakdownRow,
FinancialHistoryWindow,
FinancialStatementKind,
TaxonomyStatementRow
} from '@/lib/types';
type LoadOptions = {
cursor?: string | null;
append?: boolean;
};
const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: string }> = [
{ value: 'thousands', label: 'Thousands (K)' },
{ value: 'millions', label: 'Millions (M)' },
{ value: 'billions', label: 'Billions (B)' }
];
const STATEMENT_OPTIONS: Array<{ value: FinancialStatementKind; label: string }> = [
{ value: 'income', label: 'Income' },
{ value: 'balance', label: 'Balance Sheet' },
{ value: 'cash_flow', label: 'Cash Flow' },
{ value: 'equity', label: 'Equity' },
{ value: 'comprehensive_income', label: 'Comprehensive Income' }
];
const WINDOW_OPTIONS: Array<{ value: FinancialHistoryWindow; label: string }> = [
{ value: '10y', label: '10 Years' },
{ value: 'all', label: 'Full Available' }
];
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)';
type OverviewPoint = {
periodId: string;
filingDate: string;
periodEnd: string | null;
label: string;
revenue: number | null;
netIncome: number | null;
totalAssets: number | null;
cash: number | null;
debt: number | null;
};
function formatLongDate(value: string) {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return 'Unknown';
}
return format(parsed, 'MMM dd, yyyy');
}
function formatShortDate(value: string) {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return 'Unknown';
}
return format(parsed, 'MMM yyyy');
}
function asDisplayCurrency(value: number | null, scale: NumberScaleUnit) {
return value === null ? 'n/a' : formatCurrencyByScale(value, scale);
}
function asAxisCurrencyTick(value: number, scale: NumberScaleUnit) {
return formatCurrencyByScale(value, scale, { maximumFractionDigits: 1 });
}
function asTooltipCurrency(value: unknown, scale: NumberScaleUnit) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return 'n/a';
}
return formatCurrencyByScale(numeric, scale);
}
function ratioPercent(numerator: number | null, denominator: number | null) {
if (numerator === null || denominator === null || denominator === 0) {
return null;
}
return (numerator / denominator) * 100;
}
function rowValue(row: { values: Record<string, number | null> }, periodId: string) {
return periodId in row.values ? row.values[periodId] : null;
}
function mergeFinancialPages(
base: CompanyFinancialStatementsResponse | null,
next: CompanyFinancialStatementsResponse
) {
if (!base) {
return next;
}
const periods = [...base.periods, ...next.periods]
.filter((period, index, list) => list.findIndex((item) => item.id === period.id) === index)
.sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate));
const rowMap = new Map<string, TaxonomyStatementRow>();
for (const row of [...base.rows, ...next.rows]) {
const existing = rowMap.get(row.key);
if (!existing) {
rowMap.set(row.key, {
...row,
values: { ...row.values },
units: { ...row.units },
sourceFactIds: [...row.sourceFactIds]
});
continue;
}
existing.hasDimensions = existing.hasDimensions || row.hasDimensions;
existing.order = Math.min(existing.order, row.order);
existing.depth = Math.min(existing.depth, row.depth);
for (const [periodId, value] of Object.entries(row.values)) {
if (!(periodId in existing.values)) {
existing.values[periodId] = value;
}
}
for (const [periodId, unit] of Object.entries(row.units)) {
if (!(periodId in existing.units)) {
existing.units[periodId] = unit;
}
}
for (const factId of row.sourceFactIds) {
if (!existing.sourceFactIds.includes(factId)) {
existing.sourceFactIds.push(factId);
}
}
}
const dimensionBreakdown = (() => {
if (!base.dimensionBreakdown && !next.dimensionBreakdown) {
return null;
}
const map = new Map<string, DimensionBreakdownRow[]>();
for (const source of [base.dimensionBreakdown, next.dimensionBreakdown]) {
if (!source) {
continue;
}
for (const [key, rows] of Object.entries(source)) {
const existing = map.get(key);
if (existing) {
existing.push(...rows);
} else {
map.set(key, [...rows]);
}
}
}
return Object.fromEntries(map.entries());
})();
return {
...next,
periods,
rows: [...rowMap.values()],
nextCursor: next.nextCursor,
coverage: {
...next.coverage,
filings: periods.length,
rows: rowMap.size,
dimensions: dimensionBreakdown
? Object.values(dimensionBreakdown).reduce((total, rows) => total + rows.length, 0)
: 0,
facts: next.coverage.facts
},
dataSourceStatus: {
...next.dataSourceStatus,
queuedSync: base.dataSourceStatus.queuedSync || next.dataSourceStatus.queuedSync
},
dimensionBreakdown
};
}
function findRowByLocalNames(rows: TaxonomyStatementRow[], localNames: string[]) {
const exact = rows.find((row) => localNames.some((name) => row.localName.toLowerCase() === name.toLowerCase()));
if (exact) {
return exact;
}
const exactLabel = rows.find((row) => localNames.some((name) => row.label.toLowerCase() === name.toLowerCase()));
if (exactLabel) {
return exactLabel;
}
return rows.find((row) => {
const haystack = `${row.key} ${row.label} ${row.localName} ${row.qname}`.toLowerCase();
return localNames.some((name) => haystack.includes(name.toLowerCase()));
}) ?? null;
}
function buildOverviewSeries(
incomeData: CompanyFinancialStatementsResponse | null,
balanceData: CompanyFinancialStatementsResponse | null
): OverviewPoint[] {
const periodMap = new Map<string, { filingDate: string; periodEnd: string | null }>();
for (const source of [incomeData, balanceData]) {
for (const period of source?.periods ?? []) {
periodMap.set(period.id, {
filingDate: period.filingDate,
periodEnd: period.periodEnd
});
}
}
const periods = [...periodMap.entries()]
.map(([periodId, data]) => ({
periodId,
filingDate: data.filingDate,
periodEnd: data.periodEnd
}))
.sort((a, b) => Date.parse(a.periodEnd ?? a.filingDate) - Date.parse(b.periodEnd ?? b.filingDate));
const incomeRows = incomeData?.rows ?? [];
const balanceRows = balanceData?.rows ?? [];
const revenueRow = findRowByLocalNames(incomeRows, [
'RevenueFromContractWithCustomerExcludingAssessedTax',
'Revenues',
'SalesRevenueNet',
'Revenue'
]);
const netIncomeRow = findRowByLocalNames(incomeRows, ['NetIncomeLoss', 'ProfitLoss']);
const assetsRow = findRowByLocalNames(balanceRows, ['Assets']);
const cashRow = findRowByLocalNames(balanceRows, [
'CashAndCashEquivalentsAtCarryingValue',
'CashCashEquivalentsAndShortTermInvestments',
'CashAndShortTermInvestments',
'Cash'
]);
const debtRow = findRowByLocalNames(balanceRows, ['LongTermDebt', 'Debt', 'LongTermDebtNoncurrent']);
return periods.map((period) => ({
periodId: period.periodId,
filingDate: period.filingDate,
periodEnd: period.periodEnd,
label: formatShortDate(period.periodEnd ?? period.filingDate),
revenue: revenueRow ? rowValue(revenueRow, period.periodId) : null,
netIncome: netIncomeRow ? rowValue(netIncomeRow, period.periodId) : null,
totalAssets: assetsRow ? rowValue(assetsRow, period.periodId) : null,
cash: cashRow ? rowValue(cashRow, period.periodId) : null,
debt: debtRow ? rowValue(debtRow, period.periodId) : null
}));
}
export default function FinancialsPage() {
return (
<Suspense fallback={<div className="flex min-h-screen items-center justify-center text-sm text-[color:var(--terminal-muted)]">Loading financial terminal...</div>}>
<FinancialsPageContent />
</Suspense>
);
}
function FinancialsPageContent() {
const { isPending, isAuthenticated } = useAuthGuard();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const { prefetchResearchTicker } = useLinkPrefetch();
const [tickerInput, setTickerInput] = useState('MSFT');
const [ticker, setTicker] = useState('MSFT');
const [statement, setStatement] = useState<FinancialStatementKind>('income');
const [window, setWindow] = useState<FinancialHistoryWindow>('10y');
const [valueScale, setValueScale] = useState<NumberScaleUnit>('millions');
const [financials, setFinancials] = useState<CompanyFinancialStatementsResponse | null>(null);
const [overviewIncome, setOverviewIncome] = useState<CompanyFinancialStatementsResponse | null>(null);
const [overviewBalance, setOverviewBalance] = useState<CompanyFinancialStatementsResponse | null>(null);
const [selectedRowKey, setSelectedRowKey] = useState<string | null>(null);
const [dimensionsEnabled, setDimensionsEnabled] = useState(false);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [syncingFinancials, setSyncingFinancials] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fromQuery = searchParams.get('ticker');
if (!fromQuery) {
return;
}
const normalized = fromQuery.trim().toUpperCase();
if (!normalized) {
return;
}
setTickerInput(normalized);
setTicker(normalized);
}, [searchParams]);
const loadOverview = useCallback(async (symbol: string, selectedWindow: FinancialHistoryWindow) => {
const [incomeResponse, balanceResponse] = await Promise.all([
queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
ticker: symbol,
statement: 'income',
window: selectedWindow,
includeDimensions: false,
includeFacts: false,
limit: selectedWindow === 'all' ? 120 : 80
})),
queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
ticker: symbol,
statement: 'balance',
window: selectedWindow,
includeDimensions: false,
includeFacts: false,
limit: selectedWindow === 'all' ? 120 : 80
}))
]);
setOverviewIncome(incomeResponse.financials);
setOverviewBalance(balanceResponse.financials);
}, [queryClient]);
const loadFinancials = useCallback(async (symbol: string, options?: LoadOptions) => {
const normalizedTicker = symbol.trim().toUpperCase();
const nextCursor = options?.cursor ?? null;
const includeDimensions = dimensionsEnabled || selectedRowKey !== null;
if (!options?.append) {
setLoading(true);
} else {
setLoadingMore(true);
}
setError(null);
try {
const response = await queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
ticker: normalizedTicker,
statement,
window,
includeDimensions,
includeFacts: false,
cursor: nextCursor,
limit: window === 'all' ? 60 : 80
}));
setFinancials((current) => {
if (options?.append) {
return mergeFinancialPages(current, response.financials);
}
return response.financials;
});
await loadOverview(normalizedTicker, window);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to load financial history');
if (!options?.append) {
setFinancials(null);
}
} finally {
setLoading(false);
setLoadingMore(false);
}
}, [
queryClient,
statement,
window,
dimensionsEnabled,
selectedRowKey,
loadOverview
]);
const syncFinancials = useCallback(async () => {
const targetTicker = (financials?.company.ticker ?? ticker).trim().toUpperCase();
if (!targetTicker) {
return;
}
setSyncingFinancials(true);
setError(null);
try {
await queueFilingSync({ ticker: targetTicker, limit: 20 });
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
void queryClient.invalidateQueries({ queryKey: ['filings'] });
void queryClient.invalidateQueries({ queryKey: ['financials-v3'] });
await loadFinancials(targetTicker);
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to queue financial sync for ${targetTicker}`);
} finally {
setSyncingFinancials(false);
}
}, [financials?.company.ticker, ticker, queryClient, loadFinancials]);
useEffect(() => {
if (!isPending && isAuthenticated) {
void loadFinancials(ticker);
}
}, [isPending, isAuthenticated, ticker, statement, window, dimensionsEnabled, loadFinancials]);
const periods = useMemo(() => {
return [...(financials?.periods ?? [])]
.sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate));
}, [financials?.periods]);
const statementRows = useMemo(() => financials?.rows ?? [], [financials?.rows]);
const overviewSeries = useMemo(() => {
return buildOverviewSeries(overviewIncome, overviewBalance);
}, [overviewIncome, overviewBalance]);
const latestOverview = overviewSeries[overviewSeries.length - 1] ?? null;
const latestTaxonomyMetrics = financials?.metrics.taxonomy
?? overviewIncome?.metrics.taxonomy
?? overviewBalance?.metrics.taxonomy
?? null;
const latestRevenue = latestOverview?.revenue ?? latestTaxonomyMetrics?.revenue ?? null;
const latestNetIncome = latestOverview?.netIncome ?? latestTaxonomyMetrics?.netIncome ?? null;
const latestTotalAssets = latestOverview?.totalAssets ?? latestTaxonomyMetrics?.totalAssets ?? null;
const latestCash = latestOverview?.cash ?? latestTaxonomyMetrics?.cash ?? null;
const latestDebt = latestOverview?.debt ?? latestTaxonomyMetrics?.debt ?? null;
const latestReferenceDate = latestOverview?.filingDate ?? periods[periods.length - 1]?.filingDate ?? null;
const selectedRow = useMemo(() => {
if (!selectedRowKey) {
return null;
}
return statementRows.find((row) => row.key === selectedRowKey) ?? null;
}, [selectedRowKey, statementRows]);
const dimensionRows = useMemo(() => {
if (!selectedRow || !financials?.dimensionBreakdown) {
return [];
}
const direct = financials.dimensionBreakdown[selectedRow.key] ?? [];
if (direct.length > 0) {
return direct;
}
const conceptKey = selectedRow.qname.toLowerCase();
for (const rows of Object.values(financials.dimensionBreakdown)) {
const matched = rows.filter((row) => (row.concept ?? '').toLowerCase() === conceptKey);
if (matched.length > 0) {
return matched;
}
}
return [];
}, [selectedRow, financials?.dimensionBreakdown]);
const selectedScaleLabel = useMemo(() => {
return FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === valueScale)?.label ?? 'Millions (M)';
}, [valueScale]);
const controlSections = useMemo<FinancialControlSection[]>(() => [
{
id: 'statement',
label: 'Statement',
value: statement,
options: STATEMENT_OPTIONS,
onChange: (nextValue) => {
setStatement(nextValue as FinancialStatementKind);
setSelectedRowKey(null);
}
},
{
id: 'history',
label: 'Window',
value: window,
options: WINDOW_OPTIONS,
onChange: (nextValue) => {
setWindow(nextValue as FinancialHistoryWindow);
setSelectedRowKey(null);
}
},
{
id: 'scale',
label: 'Scale',
value: valueScale,
options: FINANCIAL_VALUE_SCALE_OPTIONS,
onChange: (nextValue) => setValueScale(nextValue as NumberScaleUnit)
}
], [statement, window, valueScale]);
const controlActions = useMemo<FinancialControlAction[]>(() => {
const actions: FinancialControlAction[] = [];
if (window === '10y') {
actions.push({
id: 'load-full-history',
label: 'Load Full History',
variant: 'secondary',
onClick: () => {
setWindow('all');
setSelectedRowKey(null);
}
});
}
if (window === 'all' && financials?.nextCursor) {
actions.push({
id: 'load-older-periods',
label: loadingMore ? 'Loading Older...' : 'Load Older Periods',
variant: 'secondary',
disabled: loadingMore,
onClick: () => {
if (!financials.nextCursor) {
return;
}
void loadFinancials(ticker, {
cursor: financials.nextCursor,
append: true
});
}
});
}
return actions;
}, [window, financials?.nextCursor, loadingMore, loadFinancials, ticker]);
if (isPending || !isAuthenticated) {
return <div className="flex min-h-screen items-center justify-center text-sm text-[color:var(--terminal-muted)]">Loading financial terminal...</div>;
}
return (
<AppShell
title="Financials"
subtitle="Taxonomy-native financial statements with PDF LLM metric validation."
activeTicker={financials?.company.ticker ?? ticker}
actions={(
<div className="flex w-full flex-wrap items-center justify-end gap-2 sm:w-auto">
<Button
variant="secondary"
disabled={syncingFinancials}
onClick={() => {
void syncFinancials();
}}
>
<Download className="size-4" />
{syncingFinancials ? 'Syncing...' : 'Sync Financials'}
</Button>
<Button
variant="secondary"
onClick={() => {
void loadFinancials(ticker);
}}
>
<RefreshCcw className="size-4" />
Refresh
</Button>
</div>
)}
>
<Panel title="Company Selector" subtitle="Load statement history by ticker. Default window is 10 years; full history is on demand.">
<form
className="flex flex-wrap items-center gap-3"
onSubmit={(event) => {
event.preventDefault();
const normalized = tickerInput.trim().toUpperCase();
if (!normalized) {
return;
}
setTicker(normalized);
}}
>
<Input
value={tickerInput}
onChange={(event) => setTickerInput(event.target.value.toUpperCase())}
placeholder="Ticker (AAPL)"
className="max-w-xs"
/>
<Button type="submit">
<Search className="size-4" />
Load Financials
</Button>
{financials ? (
<Link
href={`/analysis?ticker=${financials.company.ticker}`}
onMouseEnter={() => prefetchResearchTicker(financials.company.ticker)}
onFocus={() => prefetchResearchTicker(financials.company.ticker)}
className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
>
Open analysis
</Link>
) : null}
</form>
</Panel>
{error ? (
<Panel>
<p className="text-sm text-[#ffb5b5]">{error}</p>
</Panel>
) : null}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<MetricCard
label="Latest Revenue"
value={asDisplayCurrency(latestRevenue, valueScale)}
delta={latestReferenceDate ? formatLongDate(latestReferenceDate) : 'No history'}
/>
<MetricCard
label="Latest Net Income"
value={asDisplayCurrency(latestNetIncome, valueScale)}
delta={latestReferenceDate ? `Net margin ${formatPercent(ratioPercent(latestNetIncome, latestRevenue) ?? 0)}` : 'No history'}
positive={(latestNetIncome ?? 0) >= 0}
/>
<MetricCard
label="Latest Total Assets"
value={asDisplayCurrency(latestTotalAssets, valueScale)}
delta={latestReferenceDate ? `Debt ${asDisplayCurrency(latestDebt, valueScale)}` : 'No history'}
/>
<MetricCard
label="Cash / Debt"
value={latestCash !== null && latestDebt !== null && latestDebt !== 0
? `${(latestCash / latestDebt).toFixed(2)}x`
: 'n/a'}
delta={latestReferenceDate ? `Cash ${asDisplayCurrency(latestCash, valueScale)}` : 'No history'}
/>
</div>
<FinancialControlBar
title="Financial Controls"
subtitle={`Taxonomy statement controls. Current display scale: ${selectedScaleLabel}.`}
sections={controlSections}
actions={controlActions}
/>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<Panel title="Income Trend" subtitle="Overview chart from taxonomy-derived income and balance concepts.">
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading overview chart...</p>
) : overviewSeries.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No income history available yet.</p>
) : (
<div className="h-[320px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={overviewSeries}>
<CartesianGrid strokeDasharray="2 2" stroke={CHART_GRID} />
<XAxis dataKey="label" stroke={CHART_MUTED} fontSize={12} minTickGap={20} />
<YAxis
stroke={CHART_MUTED}
fontSize={12}
tickFormatter={(value: number) => asAxisCurrencyTick(value, valueScale)}
/>
<Tooltip
formatter={(value) => asTooltipCurrency(value, valueScale)}
contentStyle={{
backgroundColor: CHART_TOOLTIP_BG,
border: `1px solid ${CHART_TOOLTIP_BORDER}`,
borderRadius: '0.75rem'
}}
/>
<Bar dataKey="revenue" name="Revenue" fill="#68ffd5" radius={[4, 4, 0, 0]} />
<Bar dataKey="netIncome" name="Net Income" fill="#5fd3ff" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
)}
</Panel>
<Panel title="Balance Trend" subtitle="Assets, cash, and debt from taxonomy-derived balance concepts.">
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading balance chart...</p>
) : overviewSeries.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No balance history available yet.</p>
) : (
<div className="h-[320px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={overviewSeries}>
<CartesianGrid strokeDasharray="2 2" stroke={CHART_GRID} />
<XAxis dataKey="label" stroke={CHART_MUTED} fontSize={12} minTickGap={20} />
<YAxis
stroke={CHART_MUTED}
fontSize={12}
tickFormatter={(value: number) => asAxisCurrencyTick(value, valueScale)}
/>
<Tooltip
formatter={(value) => asTooltipCurrency(value, valueScale)}
contentStyle={{
backgroundColor: CHART_TOOLTIP_BG,
border: `1px solid ${CHART_TOOLTIP_BORDER}`,
borderRadius: '0.75rem'
}}
/>
<Area type="monotone" dataKey="totalAssets" name="Total Assets" stroke="#68ffd5" fill="rgba(104,255,213,0.18)" strokeWidth={2} />
<Line type="monotone" dataKey="cash" name="Cash" stroke="#8bd3ff" strokeWidth={2} dot={false} />
<Line type="monotone" dataKey="debt" name="Debt" stroke="#ffd08a" strokeWidth={2} dot={false} />
</AreaChart>
</ResponsiveContainer>
</div>
)}
</Panel>
</div>
<Panel
title="Statement Matrix"
subtitle={`Taxonomy-native ${statement.replace('_', ' ')} rows by period. Extension concepts are tagged; rows with dimensions can be drilled down.`}
>
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading statement matrix...</p>
) : periods.length === 0 || statementRows.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No statement rows available for the selected filters yet.</p>
) : (
<div className="overflow-x-auto">
<table className="data-table min-w-[980px]">
<thead>
<tr>
<th className="sticky left-0 z-10 bg-[color:var(--panel)]">Metric</th>
{periods.map((period) => (
<th key={period.id}>
<div className="flex flex-col gap-1">
<span>{formatLongDate(period.filingDate)}</span>
<span className="text-[11px] normal-case tracking-normal text-[color:var(--terminal-muted)]">{period.filingType} · {period.periodLabel}</span>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{statementRows.map((row) => (
<tr
key={row.key}
className={selectedRowKey === row.key ? 'bg-[color:var(--panel-soft)]' : undefined}
onClick={() => {
setSelectedRowKey(row.key);
if (row.hasDimensions && !dimensionsEnabled) {
setDimensionsEnabled(true);
}
}}
>
<td className="sticky left-0 z-10 bg-[color:var(--panel)]">
<div className="flex items-center gap-2">
<span style={{ paddingLeft: `${Math.min(row.depth, 10) * 12}px` }}>{row.label}</span>
{row.isExtension ? <span className="rounded border border-[color:var(--line-weak)] px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-[color:var(--terminal-muted)]">Ext</span> : null}
{row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
</div>
</td>
{periods.map((period) => (
<td key={`${row.key}-${period.id}`}>
{asDisplayCurrency(rowValue(row, period.id), valueScale)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</Panel>
<Panel title="Dimension Drill-down" subtitle="Segment/geography/product axes are shown only for the selected row when available.">
{!selectedRow ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Select a statement row to inspect dimensional facts.</p>
) : !selectedRow.hasDimensions ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No dimensional data is available for {selectedRow.label}.</p>
) : !dimensionsEnabled ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Enable dimensions by selecting the row again.</p>
) : dimensionRows.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Dimensions are still loading or unavailable for this row.</p>
) : (
<div className="overflow-x-auto">
<table className="data-table min-w-[760px]">
<thead>
<tr>
<th>Period</th>
<th>Axis</th>
<th>Member</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{dimensionRows.map((row, index) => {
const period = periods.find((item) => item.id === row.periodId);
return (
<tr key={`${row.rowKey}-${row.periodId}-${row.axis}-${row.member}-${index}`}>
<td>{period ? formatLongDate(period.filingDate) : row.periodId}</td>
<td>{row.axis}</td>
<td>{row.member}</td>
<td>{asDisplayCurrency(row.value, valueScale)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</Panel>
{financials ? (
<Panel title="Metric Validation" subtitle="Taxonomy-derived metrics are canonical; PDF LLM extraction is used for validation only.">
<div className="mb-3 flex items-center gap-2 text-sm text-[color:var(--terminal-muted)]">
<AlertTriangle className="size-4" />
<span>Overall status: {financials.metrics.validation?.status ?? 'not_run'}</span>
</div>
{(financials.metrics.validation?.checks.length ?? 0) === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No validation checks available yet.</p>
) : (
<div className="overflow-x-auto">
<table className="data-table min-w-[760px]">
<thead>
<tr>
<th>Metric</th>
<th>Taxonomy</th>
<th>LLM (PDF)</th>
<th>Status</th>
<th>Pages</th>
</tr>
</thead>
<tbody>
{financials.metrics.validation?.checks.map((check) => (
<tr key={check.metricKey}>
<td>{check.metricKey}</td>
<td>{asDisplayCurrency(check.taxonomyValue, valueScale)}</td>
<td>{asDisplayCurrency(check.llmValue, valueScale)}</td>
<td>{check.status}</td>
<td>{check.evidencePages.join(', ') || 'n/a'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Panel>
) : null}
{financials ? (
<Panel title="Data Source Status" subtitle="Hydration and taxonomy parsing status for filing snapshots.">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm">
<p className="text-[color:var(--terminal-muted)]">Hydrated</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{financials.dataSourceStatus.hydratedFilings}</p>
</div>
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm">
<p className="text-[color:var(--terminal-muted)]">Partial</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{financials.dataSourceStatus.partialFilings}</p>
</div>
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm">
<p className="text-[color:var(--terminal-muted)]">Failed</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{financials.dataSourceStatus.failedFilings}</p>
</div>
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm">
<p className="text-[color:var(--terminal-muted)]">Pending</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{financials.dataSourceStatus.pendingFilings}</p>
</div>
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm">
<p className="text-[color:var(--terminal-muted)]">Background Sync</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{financials.dataSourceStatus.queuedSync ? 'Queued' : 'Idle'}</p>
</div>
</div>
</Panel>
) : null}
<Panel>
<div className="flex items-center gap-2 text-xs uppercase tracking-[0.24em] text-[color:var(--terminal-muted)]">
<ChartNoAxesCombined className="size-4" />
Financial Statements V3: taxonomy + PDF LLM validation
</div>
</Panel>
</AppShell>
);
}