Run playwright UI tests
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
XAxis,
|
||||
YAxis
|
||||
} from 'recharts';
|
||||
import { ChartNoAxesCombined, ChevronDown, Download, RefreshCcw, Search } from 'lucide-react';
|
||||
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 {
|
||||
@@ -41,11 +41,9 @@ import { companyFinancialStatementsQueryOptions } from '@/lib/query/options';
|
||||
import type {
|
||||
CompanyFinancialStatementsResponse,
|
||||
DimensionBreakdownRow,
|
||||
FilingFaithfulStatementRow,
|
||||
FinancialHistoryWindow,
|
||||
FinancialStatementKind,
|
||||
FinancialStatementMode,
|
||||
StandardizedStatementRow
|
||||
TaxonomyStatementRow
|
||||
} from '@/lib/types';
|
||||
|
||||
type LoadOptions = {
|
||||
@@ -59,11 +57,6 @@ const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: stri
|
||||
{ value: 'billions', label: 'Billions (B)' }
|
||||
];
|
||||
|
||||
const MODE_OPTIONS: Array<{ value: FinancialStatementMode; label: string }> = [
|
||||
{ value: 'standardized', label: 'Standardized' },
|
||||
{ value: 'filing_faithful', label: 'Filing-faithful' }
|
||||
];
|
||||
|
||||
const STATEMENT_OPTIONS: Array<{ value: FinancialStatementKind; label: string }> = [
|
||||
{ value: 'income', label: 'Income' },
|
||||
{ value: 'balance', label: 'Balance Sheet' },
|
||||
@@ -85,6 +78,7 @@ 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;
|
||||
@@ -136,22 +130,6 @@ function ratioPercent(numerator: number | null, denominator: number | null) {
|
||||
return (numerator / denominator) * 100;
|
||||
}
|
||||
|
||||
function toStandardizedRows(data: CompanyFinancialStatementsResponse | null) {
|
||||
if (!data || data.mode !== 'standardized') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.rows as StandardizedStatementRow[];
|
||||
}
|
||||
|
||||
function toFilingFaithfulRows(data: CompanyFinancialStatementsResponse | null) {
|
||||
if (!data || data.mode !== 'filing_faithful') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.rows as FilingFaithfulStatementRow[];
|
||||
}
|
||||
|
||||
function rowValue(row: { values: Record<string, number | null> }, periodId: string) {
|
||||
return periodId in row.values ? row.values[periodId] : null;
|
||||
}
|
||||
@@ -168,30 +146,39 @@ function mergeFinancialPages(
|
||||
.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, StandardizedStatementRow | FilingFaithfulStatementRow>();
|
||||
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 }
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if ('sourceConcepts' in existing && 'sourceConcepts' in row) {
|
||||
for (const concept of row.sourceConcepts) {
|
||||
if (!existing.sourceConcepts.includes(concept)) {
|
||||
existing.sourceConcepts.push(concept);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,21 +207,19 @@ function mergeFinancialPages(
|
||||
return Object.fromEntries(map.entries());
|
||||
})();
|
||||
|
||||
const mergedRows = [...rowMap.values()];
|
||||
|
||||
return {
|
||||
...next,
|
||||
periods,
|
||||
rows: next.mode === 'standardized'
|
||||
? mergedRows as StandardizedStatementRow[]
|
||||
: mergedRows as FilingFaithfulStatementRow[],
|
||||
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
|
||||
: 0,
|
||||
facts: next.coverage.facts
|
||||
},
|
||||
dataSourceStatus: {
|
||||
...next.dataSourceStatus,
|
||||
@@ -244,20 +229,20 @@ function mergeFinancialPages(
|
||||
};
|
||||
}
|
||||
|
||||
function findStandardizedRowValue(
|
||||
data: CompanyFinancialStatementsResponse | null,
|
||||
preferredKey: string,
|
||||
fallbackIncludes: string[]
|
||||
) {
|
||||
const rows = toStandardizedRows(data);
|
||||
const exact = rows.find((row) => row.key === preferredKey);
|
||||
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.concept}`.toLowerCase();
|
||||
return fallbackIncludes.some((needle) => haystack.includes(needle));
|
||||
const haystack = `${row.key} ${row.label} ${row.localName} ${row.qname}`.toLowerCase();
|
||||
return localNames.some((name) => haystack.includes(name.toLowerCase()));
|
||||
}) ?? null;
|
||||
}
|
||||
|
||||
@@ -265,28 +250,49 @@ function buildOverviewSeries(
|
||||
incomeData: CompanyFinancialStatementsResponse | null,
|
||||
balanceData: CompanyFinancialStatementsResponse | null
|
||||
): OverviewPoint[] {
|
||||
const periodMap = new Map<string, { filingDate: string }>();
|
||||
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 });
|
||||
periodMap.set(period.id, {
|
||||
filingDate: period.filingDate,
|
||||
periodEnd: period.periodEnd
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const periods = [...periodMap.entries()]
|
||||
.map(([periodId, data]) => ({ periodId, filingDate: data.filingDate }))
|
||||
.sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate));
|
||||
.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 revenueRow = findStandardizedRowValue(incomeData, 'revenue', ['revenue', 'sales']);
|
||||
const netIncomeRow = findStandardizedRowValue(incomeData, 'net-income', ['net income', 'profit']);
|
||||
const assetsRow = findStandardizedRowValue(balanceData, 'total-assets', ['total assets']);
|
||||
const cashRow = findStandardizedRowValue(balanceData, 'cash-and-equivalents', ['cash']);
|
||||
const debtRow = findStandardizedRowValue(balanceData, 'total-debt', ['debt', 'borrowings']);
|
||||
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,
|
||||
label: formatShortDate(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,
|
||||
@@ -311,7 +317,6 @@ function FinancialsPageContent() {
|
||||
|
||||
const [tickerInput, setTickerInput] = useState('MSFT');
|
||||
const [ticker, setTicker] = useState('MSFT');
|
||||
const [mode, setMode] = useState<FinancialStatementMode>('standardized');
|
||||
const [statement, setStatement] = useState<FinancialStatementKind>('income');
|
||||
const [window, setWindow] = useState<FinancialHistoryWindow>('10y');
|
||||
const [valueScale, setValueScale] = useState<NumberScaleUnit>('millions');
|
||||
@@ -344,18 +349,18 @@ function FinancialsPageContent() {
|
||||
const [incomeResponse, balanceResponse] = await Promise.all([
|
||||
queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
|
||||
ticker: symbol,
|
||||
mode: 'standardized',
|
||||
statement: 'income',
|
||||
window: selectedWindow,
|
||||
includeDimensions: false,
|
||||
includeFacts: false,
|
||||
limit: selectedWindow === 'all' ? 120 : 80
|
||||
})),
|
||||
queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
|
||||
ticker: symbol,
|
||||
mode: 'standardized',
|
||||
statement: 'balance',
|
||||
window: selectedWindow,
|
||||
includeDimensions: false,
|
||||
includeFacts: false,
|
||||
limit: selectedWindow === 'all' ? 120 : 80
|
||||
}))
|
||||
]);
|
||||
@@ -380,10 +385,10 @@ function FinancialsPageContent() {
|
||||
try {
|
||||
const response = await queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
|
||||
ticker: normalizedTicker,
|
||||
mode,
|
||||
statement,
|
||||
window,
|
||||
includeDimensions,
|
||||
includeFacts: false,
|
||||
cursor: nextCursor,
|
||||
limit: window === 'all' ? 60 : 80
|
||||
}));
|
||||
@@ -408,7 +413,6 @@ function FinancialsPageContent() {
|
||||
}
|
||||
}, [
|
||||
queryClient,
|
||||
mode,
|
||||
statement,
|
||||
window,
|
||||
dimensionsEnabled,
|
||||
@@ -429,7 +433,7 @@ function FinancialsPageContent() {
|
||||
await queueFilingSync({ ticker: targetTicker, limit: 20 });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
|
||||
void queryClient.invalidateQueries({ queryKey: ['filings'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['financials-v2'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['financials-v3'] });
|
||||
await loadFinancials(targetTicker);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : `Failed to queue financial sync for ${targetTicker}`);
|
||||
@@ -442,25 +446,30 @@ function FinancialsPageContent() {
|
||||
if (!isPending && isAuthenticated) {
|
||||
void loadFinancials(ticker);
|
||||
}
|
||||
}, [isPending, isAuthenticated, ticker, mode, statement, window, dimensionsEnabled, loadFinancials]);
|
||||
}, [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 standardizedRows = useMemo(() => toStandardizedRows(financials), [financials]);
|
||||
const filingFaithfulRows = useMemo(() => toFilingFaithfulRows(financials), [financials]);
|
||||
|
||||
const statementRows = mode === 'standardized'
|
||||
? standardizedRows
|
||||
: filingFaithfulRows;
|
||||
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) {
|
||||
@@ -480,13 +489,11 @@ function FinancialsPageContent() {
|
||||
return direct;
|
||||
}
|
||||
|
||||
if ('concept' in selectedRow && selectedRow.concept) {
|
||||
const conceptKey = selectedRow.concept.toLowerCase();
|
||||
for (const rows of Object.values(financials.dimensionBreakdown)) {
|
||||
const matched = rows.filter((row) => (row.concept ?? '').toLowerCase() === conceptKey);
|
||||
if (matched.length > 0) {
|
||||
return matched;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,16 +505,6 @@ function FinancialsPageContent() {
|
||||
}, [valueScale]);
|
||||
|
||||
const controlSections = useMemo<FinancialControlSection[]>(() => [
|
||||
{
|
||||
id: 'mode',
|
||||
label: 'Mode',
|
||||
value: mode,
|
||||
options: MODE_OPTIONS,
|
||||
onChange: (nextValue) => {
|
||||
setMode(nextValue as FinancialStatementMode);
|
||||
setSelectedRowKey(null);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'statement',
|
||||
label: 'Statement',
|
||||
@@ -535,7 +532,7 @@ function FinancialsPageContent() {
|
||||
options: FINANCIAL_VALUE_SCALE_OPTIONS,
|
||||
onChange: (nextValue) => setValueScale(nextValue as NumberScaleUnit)
|
||||
}
|
||||
], [mode, statement, window, valueScale]);
|
||||
], [statement, window, valueScale]);
|
||||
|
||||
const controlActions = useMemo<FinancialControlAction[]>(() => {
|
||||
const actions: FinancialControlAction[] = [];
|
||||
@@ -581,7 +578,7 @@ function FinancialsPageContent() {
|
||||
return (
|
||||
<AppShell
|
||||
title="Financials"
|
||||
subtitle="Dual-mode financial statements with standardized comparability and filing-faithful presentation."
|
||||
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">
|
||||
@@ -651,42 +648,42 @@ function FinancialsPageContent() {
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<MetricCard
|
||||
label="Latest Revenue"
|
||||
value={latestOverview ? asDisplayCurrency(latestOverview.revenue, valueScale) : 'n/a'}
|
||||
delta={latestOverview ? formatLongDate(latestOverview.filingDate) : 'No standardized history'}
|
||||
value={asDisplayCurrency(latestRevenue, valueScale)}
|
||||
delta={latestReferenceDate ? formatLongDate(latestReferenceDate) : 'No history'}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Latest Net Income"
|
||||
value={latestOverview ? asDisplayCurrency(latestOverview.netIncome, valueScale) : 'n/a'}
|
||||
delta={latestOverview ? `Net margin ${formatPercent(ratioPercent(latestOverview.netIncome, latestOverview.revenue) ?? 0)}` : 'No standardized history'}
|
||||
positive={(latestOverview?.netIncome ?? 0) >= 0}
|
||||
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={latestOverview ? asDisplayCurrency(latestOverview.totalAssets, valueScale) : 'n/a'}
|
||||
delta={latestOverview ? `Debt ${asDisplayCurrency(latestOverview.debt, valueScale)}` : 'No standardized history'}
|
||||
value={asDisplayCurrency(latestTotalAssets, valueScale)}
|
||||
delta={latestReferenceDate ? `Debt ${asDisplayCurrency(latestDebt, valueScale)}` : 'No history'}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Cash / Debt"
|
||||
value={latestOverview && latestOverview.cash !== null && latestOverview.debt !== null && latestOverview.debt !== 0
|
||||
? `${(latestOverview.cash / latestOverview.debt).toFixed(2)}x`
|
||||
value={latestCash !== null && latestDebt !== null && latestDebt !== 0
|
||||
? `${(latestCash / latestDebt).toFixed(2)}x`
|
||||
: 'n/a'}
|
||||
delta={latestOverview ? `Cash ${asDisplayCurrency(latestOverview.cash, valueScale)}` : 'No standardized history'}
|
||||
delta={latestReferenceDate ? `Cash ${asDisplayCurrency(latestCash, valueScale)}` : 'No history'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FinancialControlBar
|
||||
title="Financial Controls"
|
||||
subtitle={`Compact multi-filter control bar. Current display scale: ${selectedScaleLabel}.`}
|
||||
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="Standardized Income Trend" subtitle="Overview chart remains anchored to standardized rows.">
|
||||
<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 standardized income history available yet.</p>
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No income history available yet.</p>
|
||||
) : (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
@@ -714,11 +711,11 @@ function FinancialsPageContent() {
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel title="Standardized Balance Trend" subtitle="Assets, cash, and debt from standardized balance rows.">
|
||||
<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 standardized balance history available yet.</p>
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No balance history available yet.</p>
|
||||
) : (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
@@ -750,7 +747,7 @@ function FinancialsPageContent() {
|
||||
|
||||
<Panel
|
||||
title="Statement Matrix"
|
||||
subtitle={`${mode === 'standardized' ? 'Cross-company comparable' : 'Filing-native'} ${statement.replace('_', ' ')} rows by period. Click rows with available dimensions for drill-down.`}
|
||||
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>
|
||||
@@ -785,17 +782,11 @@ function FinancialsPageContent() {
|
||||
}}
|
||||
>
|
||||
<td className="sticky left-0 z-10 bg-[color:var(--panel)]">
|
||||
{'depth' in row ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ paddingLeft: `${Math.min(row.depth, 10) * 12}px` }}>{row.label}</span>
|
||||
{row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{row.label}</span>
|
||||
{row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
|
||||
</div>
|
||||
)}
|
||||
<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}`}>
|
||||
@@ -850,7 +841,44 @@ function FinancialsPageContent() {
|
||||
</Panel>
|
||||
|
||||
{financials ? (
|
||||
<Panel title="Data Source Status" subtitle="Hydration and parsing status for filing statement snapshots.">
|
||||
<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>
|
||||
@@ -879,7 +907,7 @@ function FinancialsPageContent() {
|
||||
<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 V2: standardized + filing-faithful history
|
||||
Financial Statements V3: taxonomy + PDF LLM validation
|
||||
</div>
|
||||
</Panel>
|
||||
</AppShell>
|
||||
|
||||
Reference in New Issue
Block a user