Files
Neon-Desk/app/financials/page.tsx

931 lines
35 KiB
TypeScript

'use client';
import { useQueryClient } from '@tanstack/react-query';
import Link from 'next/link';
import { Fragment, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { format } from 'date-fns';
import { useSearchParams } from 'next/navigation';
import {
Bar,
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import {
AlertTriangle,
ChevronDown,
Download,
RefreshCcw,
Search
} from 'lucide-react';
import { AppShell } from '@/components/shell/app-shell';
import {
FinancialControlBar,
type FinancialControlAction,
type FinancialControlOption,
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 { buildGraphingHref } from '@/lib/graphing/catalog';
import { queryKeys } from '@/lib/query/keys';
import { companyFinancialStatementsQueryOptions } from '@/lib/query/options';
import type {
CompanyFinancialStatementsResponse,
DerivedFinancialRow,
FinancialCadence,
FinancialDisplayMode,
FinancialSurfaceKind,
FinancialUnit,
RatioRow,
StandardizedFinancialRow,
StructuredKpiRow,
TaxonomyStatementRow,
TrendSeries
} from '@/lib/types';
type LoadOptions = {
cursor?: string | null;
append?: boolean;
};
type DisplayRow = TaxonomyStatementRow | StandardizedFinancialRow | RatioRow | StructuredKpiRow;
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 SURFACE_OPTIONS: FinancialControlOption[] = [
{ value: 'income_statement', label: 'Income Statement' },
{ value: 'balance_sheet', label: 'Balance Sheet' },
{ value: 'cash_flow_statement', label: 'Cash Flow Statement' },
{ value: 'ratios', label: 'Ratios' },
{ value: 'segments_kpis', label: 'Segments & KPIs' },
{ value: 'adjusted', label: 'Adjusted' },
{ value: 'custom_metrics', label: 'Custom Metrics' }
];
const CADENCE_OPTIONS: FinancialControlOption[] = [
{ value: 'annual', label: 'Annual' },
{ value: 'quarterly', label: 'Quarterly' },
{ value: 'ltm', label: 'LTM' }
];
const DISPLAY_MODE_OPTIONS: FinancialControlOption[] = [
{ value: 'standardized', label: 'Standardized' },
{ value: 'faithful', label: 'Filing-faithful' }
];
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)';
function formatLongDate(value: string) {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return 'Unknown';
}
return format(parsed, 'MMM dd, yyyy');
}
function isTaxonomyRow(row: DisplayRow): row is TaxonomyStatementRow {
return 'localName' in row;
}
function isDerivedRow(row: DisplayRow): row is DerivedFinancialRow {
return 'formulaKey' in row;
}
function isKpiRow(row: DisplayRow): row is StructuredKpiRow {
return 'provenanceType' in row;
}
function rowValue(row: DisplayRow, periodId: string) {
return row.values[periodId] ?? null;
}
function formatMetricValue(value: number | null, unit: FinancialUnit, scale: NumberScaleUnit) {
if (value === null) {
return 'n/a';
}
switch (unit) {
case 'currency':
return formatCurrencyByScale(value, scale);
case 'percent':
return formatPercent(value * 100);
case 'shares':
case 'count':
return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2 }).format(value);
case 'ratio':
return `${value.toFixed(2)}x`;
default:
return String(value);
}
}
function chartTickFormatter(value: number, unit: FinancialUnit, scale: NumberScaleUnit) {
if (!Number.isFinite(value)) {
return 'n/a';
}
return formatMetricValue(value, unit, scale);
}
function buildDisplayValue(input: {
row: DisplayRow;
periodId: string;
previousPeriodId: string | null;
commonSizeRow: DisplayRow | null;
displayMode: FinancialDisplayMode;
showPercentChange: boolean;
showCommonSize: boolean;
scale: NumberScaleUnit;
surfaceKind: FinancialSurfaceKind;
}) {
const current = rowValue(input.row, input.periodId);
if (input.showPercentChange && input.previousPeriodId) {
const previous = rowValue(input.row, input.previousPeriodId);
if (current === null || previous === null || previous === 0) {
return 'n/a';
}
return formatPercent(((current - previous) / previous) * 100);
}
if (input.showCommonSize) {
if (input.surfaceKind === 'ratios' && isDerivedRow(input.row) && input.row.unit === 'percent') {
return formatPercent((current ?? 0) * 100);
}
if (input.displayMode === 'faithful') {
return 'n/a';
}
const denominator = input.commonSizeRow ? rowValue(input.commonSizeRow, input.periodId) : null;
if (current === null || denominator === null || denominator === 0) {
return 'n/a';
}
return formatPercent((current / denominator) * 100);
}
const unit = isTaxonomyRow(input.row)
? 'currency'
: input.row.unit;
return formatMetricValue(current, unit, input.scale);
}
function groupRows(rows: DisplayRow[], categories: CompanyFinancialStatementsResponse['categories']) {
if (categories.length === 0) {
return [{ label: null, rows }];
}
return categories
.map((category) => ({
label: category.label,
rows: rows.filter((row) => !isTaxonomyRow(row) && row.category === category.key)
}))
.filter((group) => group.rows.length > 0);
}
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((left, right) => Date.parse(left.periodEnd ?? left.filingDate) - Date.parse(right.periodEnd ?? right.filingDate));
const mergeRows = <T extends { key: string; values: Record<string, number | null> }>(rows: T[]) => {
const map = new Map<string, T>();
for (const row of rows) {
const existing = map.get(row.key);
if (!existing) {
map.set(row.key, structuredClone(row));
continue;
}
existing.values = {
...existing.values,
...row.values
};
}
return [...map.values()];
};
return {
...next,
periods,
statementRows: next.statementRows && base.statementRows
? {
faithful: mergeRows([...base.statementRows.faithful, ...next.statementRows.faithful]),
standardized: mergeRows([...base.statementRows.standardized, ...next.statementRows.standardized])
}
: next.statementRows,
ratioRows: next.ratioRows && base.ratioRows ? mergeRows([...base.ratioRows, ...next.ratioRows]) : next.ratioRows,
kpiRows: next.kpiRows && base.kpiRows ? mergeRows([...base.kpiRows, ...next.kpiRows]) : next.kpiRows,
trendSeries: next.trendSeries,
categories: next.categories,
dimensionBreakdown: next.dimensionBreakdown ?? base.dimensionBreakdown
};
}
function ChartFrame({ children }: { children: React.ReactNode }) {
const containerRef = useRef<HTMLDivElement | null>(null);
const [ready, setReady] = useState(false);
useEffect(() => {
const element = containerRef.current;
if (!element) {
return;
}
const observer = new ResizeObserver((entries) => {
const next = entries[0];
if (!next) {
return;
}
const { width, height } = next.contentRect;
setReady(width > 0 && height > 0);
});
observer.observe(element);
return () => observer.disconnect();
}, []);
return <div ref={containerRef} className="h-[320px]">{ready ? children : null}</div>;
}
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 [surfaceKind, setSurfaceKind] = useState<FinancialSurfaceKind>('income_statement');
const [cadence, setCadence] = useState<FinancialCadence>('annual');
const [displayMode, setDisplayMode] = useState<FinancialDisplayMode>('standardized');
const [valueScale, setValueScale] = useState<NumberScaleUnit>('millions');
const [rowSearch, setRowSearch] = useState('');
const [showPercentChange, setShowPercentChange] = useState(false);
const [showCommonSize, setShowCommonSize] = useState(false);
const [financials, setFinancials] = useState<CompanyFinancialStatementsResponse | null>(null);
const [selectedRowKey, setSelectedRowKey] = useState<string | null>(null);
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]);
useEffect(() => {
const statementSurface = surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement';
if (!statementSurface && displayMode !== 'standardized') {
setDisplayMode('standardized');
}
if (displayMode === 'faithful') {
setShowPercentChange(false);
setShowCommonSize(false);
}
if (surfaceKind === 'adjusted' || surfaceKind === 'custom_metrics') {
setShowPercentChange(false);
setShowCommonSize(false);
}
}, [displayMode, surfaceKind]);
const loadFinancials = useCallback(async (symbol: string, options?: LoadOptions) => {
const normalizedTicker = symbol.trim().toUpperCase();
const nextCursor = options?.cursor ?? null;
const includeDimensions = selectedRowKey !== null && (surfaceKind === 'segments_kpis' || surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement');
if (!options?.append) {
setLoading(true);
} else {
setLoadingMore(true);
}
setError(null);
try {
const response = await queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
ticker: normalizedTicker,
surfaceKind,
cadence,
includeDimensions,
includeFacts: false,
cursor: nextCursor,
limit: 12
}));
setFinancials((current) => options?.append ? mergeFinancialPages(current, response.financials) : response.financials);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to load financial history');
if (!options?.append) {
setFinancials(null);
}
} finally {
setLoading(false);
setLoadingMore(false);
}
}, [cadence, queryClient, selectedRowKey, surfaceKind]);
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, loadFinancials, queryClient, ticker]);
useEffect(() => {
if (!isPending && isAuthenticated) {
void loadFinancials(ticker);
}
}, [isPending, isAuthenticated, loadFinancials, ticker]);
const periods = useMemo(() => {
return [...(financials?.periods ?? [])]
.sort((left, right) => Date.parse(left.periodEnd ?? left.filingDate) - Date.parse(right.periodEnd ?? right.filingDate));
}, [financials?.periods]);
const activeRows = useMemo<DisplayRow[]>(() => {
if (!financials) {
return [];
}
switch (surfaceKind) {
case 'income_statement':
case 'balance_sheet':
case 'cash_flow_statement':
return displayMode === 'faithful'
? financials.statementRows?.faithful ?? []
: financials.statementRows?.standardized ?? [];
case 'ratios':
return financials.ratioRows ?? [];
case 'segments_kpis':
return financials.kpiRows ?? [];
default:
return [];
}
}, [displayMode, financials, surfaceKind]);
const filteredRows = useMemo(() => {
const normalizedSearch = rowSearch.trim().toLowerCase();
if (!normalizedSearch) {
return activeRows;
}
return activeRows.filter((row) => row.label.toLowerCase().includes(normalizedSearch));
}, [activeRows, rowSearch]);
const groupedRows = useMemo(() => {
return groupRows(filteredRows, financials?.categories ?? []);
}, [filteredRows, financials?.categories]);
const selectedRow = useMemo(() => {
if (!selectedRowKey) {
return null;
}
return activeRows.find((row) => row.key === selectedRowKey) ?? null;
}, [activeRows, selectedRowKey]);
const dimensionRows = useMemo(() => {
if (!selectedRow || !financials?.dimensionBreakdown) {
return [];
}
return financials.dimensionBreakdown[selectedRow.key] ?? [];
}, [financials?.dimensionBreakdown, selectedRow]);
const commonSizeRow = useMemo<DisplayRow | null>(() => {
if (displayMode === 'faithful' || !financials?.statementRows) {
return null;
}
const standardizedRows = financials.statementRows.standardized;
if (surfaceKind === 'income_statement' || surfaceKind === 'cash_flow_statement') {
return standardizedRows.find((row) => row.key === 'revenue') ?? null;
}
if (surfaceKind === 'balance_sheet') {
return standardizedRows.find((row) => row.key === 'total_assets') ?? null;
}
return null;
}, [displayMode, financials?.statementRows, surfaceKind]);
const trendSeries = financials?.trendSeries ?? [];
const chartData = useMemo(() => {
return periods.map((period) => ({
label: formatLongDate(period.periodEnd ?? period.filingDate),
...Object.fromEntries(trendSeries.map((series) => [series.key, series.values[period.id] ?? null]))
}));
}, [periods, trendSeries]);
const controlSections = useMemo<FinancialControlSection[]>(() => {
const sections: FinancialControlSection[] = [
{
id: 'surface',
label: 'Surface',
value: surfaceKind,
options: SURFACE_OPTIONS,
onChange: (value) => {
setSurfaceKind(value as FinancialSurfaceKind);
setSelectedRowKey(null);
}
},
{
id: 'cadence',
label: 'Cadence',
value: cadence,
options: CADENCE_OPTIONS,
onChange: (value) => {
setCadence(value as FinancialCadence);
setSelectedRowKey(null);
}
}
];
if (surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement') {
sections.push({
id: 'display',
label: 'Display',
value: displayMode,
options: DISPLAY_MODE_OPTIONS,
onChange: (value) => {
setDisplayMode(value as FinancialDisplayMode);
setSelectedRowKey(null);
}
});
}
sections.push({
id: 'scale',
label: 'Scale',
value: valueScale,
options: FINANCIAL_VALUE_SCALE_OPTIONS,
onChange: (value) => setValueScale(value as NumberScaleUnit)
});
return sections;
}, [cadence, displayMode, surfaceKind, valueScale]);
const controlActions = useMemo<FinancialControlAction[]>(() => {
const actions: FinancialControlAction[] = [];
if ((surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement' || surfaceKind === 'ratios') && displayMode !== 'faithful') {
actions.push({
id: 'percent-change',
label: showPercentChange ? '% Change On' : '% Change',
variant: showPercentChange ? 'primary' : 'secondary',
onClick: () => {
setShowPercentChange((current) => !current);
setShowCommonSize(false);
}
});
actions.push({
id: 'common-size',
label: showCommonSize ? 'Common Size On' : 'Common Size',
variant: showCommonSize ? 'primary' : 'secondary',
onClick: () => {
setShowCommonSize((current) => !current);
setShowPercentChange(false);
},
disabled: surfaceKind === 'ratios' && selectedRow !== null && isDerivedRow(selectedRow) && selectedRow.unit !== 'percent'
});
}
if (financials?.nextCursor) {
actions.push({
id: 'load-older',
label: loadingMore ? 'Loading Older...' : 'Load Older',
variant: 'secondary',
disabled: loadingMore,
onClick: () => {
if (!financials.nextCursor) {
return;
}
void loadFinancials(ticker, {
cursor: financials.nextCursor,
append: true
});
}
});
}
return actions;
}, [displayMode, financials?.nextCursor, loadFinancials, loadingMore, selectedRow, showCommonSize, showPercentChange, surfaceKind, 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="Surface-based filing financials with statement, ratio, and KPI time series."
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 one ticker across statements, ratios, and KPI time series.">
<form
className="flex flex-col items-stretch gap-3 sm:flex-row sm:flex-wrap sm:items-center"
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="w-full sm:max-w-xs"
/>
<Button type="submit" className="w-full sm:w-auto">
<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>
<Link
href={buildGraphingHref(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 graphing
</Link>
</>
) : null}
</form>
</Panel>
{error ? (
<Panel variant="surface">
<p className="text-sm text-[#ffb5b5]">{error}</p>
</Panel>
) : null}
<FinancialControlBar
title="Financial Controls"
subtitle="Choose surface, cadence, display mode, and presentation toggles."
sections={controlSections}
actions={controlActions}
/>
<Panel title="Search & Filters" subtitle="Filter rows without mutating the source data.">
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<Input
value={rowSearch}
onChange={(event) => setRowSearch(event.target.value)}
placeholder="Search rows by label"
className="w-full sm:max-w-sm"
/>
<span className="text-sm text-[color:var(--terminal-muted)]">
{filteredRows.length} of {activeRows.length} rows
</span>
</div>
</Panel>
<Panel title="Trend Chart" subtitle="Default trend series for the selected surface." variant="surface">
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading trend chart...</p>
) : chartData.length === 0 || trendSeries.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No trend data available for the selected surface.</p>
) : (
<ChartFrame>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<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) => chartTickFormatter(value, trendSeries[0]?.unit ?? 'currency', valueScale)}
/>
<Tooltip
formatter={(value: unknown, _name, entry) => {
const numeric = Number(value);
const unit = trendSeries.find((series) => series.key === entry.dataKey)?.unit ?? 'currency';
return formatMetricValue(Number.isFinite(numeric) ? numeric : null, unit, valueScale);
}}
contentStyle={{
backgroundColor: CHART_TOOLTIP_BG,
border: `1px solid ${CHART_TOOLTIP_BORDER}`,
borderRadius: '0.75rem'
}}
/>
{trendSeries.map((series, index) => (
<Line
key={series.key}
type="monotone"
dataKey={series.key}
name={series.label}
stroke={['#68ffd5', '#5fd3ff', '#ffd08a', '#ff8a8a'][index % 4]}
strokeWidth={2}
dot={false}
/>
))}
</LineChart>
</ResponsiveContainer>
</ChartFrame>
)}
</Panel>
<Panel title="Surface Matrix" subtitle="Standardized statements, ratios, and KPIs render in one shared matrix." variant="surface">
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading financial matrix...</p>
) : surfaceKind === 'adjusted' || surfaceKind === 'custom_metrics' ? (
<div className="rounded-xl border border-dashed border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-4 py-6">
<p className="text-sm text-[color:var(--terminal-bright)]">This surface is not yet available in v1.</p>
<p className="mt-2 text-sm text-[color:var(--terminal-muted)]">Adjusted and custom metrics are API-visible placeholders only. No edits or derived rows are available yet.</p>
</div>
) : periods.length === 0 || filteredRows.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No rows available for the selected filters yet.</p>
) : (
<div className="data-table-wrap">
<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.periodEnd ?? 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>
{groupedRows.map((group) => (
<Fragment key={group.label ?? 'ungrouped'}>
{group.label ? (
<tr className="bg-[color:var(--panel-soft)]">
<td colSpan={periods.length + 1} className="font-semibold text-[color:var(--terminal-bright)]">{group.label}</td>
</tr>
) : null}
{group.rows.map((row) => (
<tr
key={row.key}
className={selectedRowKey === row.key ? 'bg-[color:var(--panel-soft)]' : undefined}
onClick={() => setSelectedRowKey(row.key)}
>
<td className="sticky left-0 z-10 bg-[color:var(--panel)]">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span style={{ paddingLeft: `${isTaxonomyRow(row) ? Math.min(row.depth, 10) * 12 : 0}px` }}>{row.label}</span>
{'hasDimensions' in row && row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
</div>
{isDerivedRow(row) && row.formulaKey ? (
<span className="text-xs text-[color:var(--terminal-muted)]">Formula: {row.formulaKey}</span>
) : null}
{isKpiRow(row) ? (
<span className="text-xs text-[color:var(--terminal-muted)]">Provenance: {row.provenanceType}</span>
) : null}
</div>
</td>
{periods.map((period, index) => (
<td key={`${row.key}-${period.id}`}>
{buildDisplayValue({
row,
periodId: period.id,
previousPeriodId: index > 0 ? periods[index - 1]?.id ?? null : null,
commonSizeRow,
displayMode,
showPercentChange,
showCommonSize,
scale: valueScale,
surfaceKind
})}
</td>
))}
</tr>
))}
</Fragment>
))}
</tbody>
</table>
</div>
)}
</Panel>
<Panel title="Row Details" subtitle="Inspect provenance, formulas, and dimensional evidence for the selected row." variant="surface">
{!selectedRow ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Select a row to inspect details.</p>
) : (
<div className="space-y-4 text-sm">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
<p className="text-[color:var(--terminal-muted)]">Label</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.label}</p>
</div>
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
<p className="text-[color:var(--terminal-muted)]">Key</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.key}</p>
</div>
</div>
{isTaxonomyRow(selectedRow) ? (
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
<p className="text-[color:var(--terminal-muted)]">Taxonomy Concept</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.qname}</p>
</div>
) : (
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
<p className="text-[color:var(--terminal-muted)]">Category</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.category}</p>
</div>
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
<p className="text-[color:var(--terminal-muted)]">Unit</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.unit}</p>
</div>
</div>
)}
{isDerivedRow(selectedRow) ? (
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
<p className="text-[color:var(--terminal-muted)]">Source Row Keys</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceRowKeys.join(', ') || 'n/a'}</p>
<p className="mt-2 text-[color:var(--terminal-muted)]">Source Concepts</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceConcepts.join(', ') || 'n/a'}</p>
<p className="mt-2 text-[color:var(--terminal-muted)]">Source Fact IDs</p>
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceFactIds.join(', ') || 'n/a'}</p>
</div>
) : null}
{!selectedRow || !('hasDimensions' in selectedRow) || !selectedRow.hasDimensions ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No dimensional drill-down is available for this row.</p>
) : dimensionRows.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No dimensional facts were returned for the selected row.</p>
) : (
<div className="data-table-wrap">
<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) => (
<tr key={`${row.rowKey}-${row.periodId}-${row.axis}-${row.member}-${index}`}>
<td>{periods.find((period) => period.id === row.periodId)?.periodLabel ?? row.periodId}</td>
<td>{row.axis}</td>
<td>{row.member}</td>
<td>{formatMetricValue(row.value, 'currency', valueScale)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</Panel>
{(surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement') && financials ? (
<Panel title="Metric Validation" subtitle="Validation remains limited to statement-derived taxonomy metrics in v1." variant="surface">
<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="data-table-wrap">
<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>{formatMetricValue(check.taxonomyValue, 'currency', valueScale)}</td>
<td>{formatMetricValue(check.llmValue, 'currency', valueScale)}</td>
<td>{check.status}</td>
<td>{check.evidencePages.join(', ') || 'n/a'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Panel>
) : null}
</AppShell>
);
}
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>
);
}