From f4a001457252e60b897862450967de37a9e6fc71 Mon Sep 17 00:00:00 2001 From: francy51 Date: Mon, 16 Mar 2026 22:31:45 -0400 Subject: [PATCH] refactor: reorganize Financials toolbar and flatten UI - Group toolbar controls by function (Statement, Period, Mode, Scale) - Move Trend Chart above Matrix, hidden by default - Add chart toggle to toolbar actions - Flatten all sections: remove card styling, use subtle dividers - Update StatementRowInspector and NormalizationSummary with flat layout --- app/financials/page.tsx | 1134 ++++++++++------- components/dashboard/index-card-row.tsx | 2 + components/financials/financials-toolbar.tsx | 230 +++- .../financials/normalization-summary.tsx | 108 +- .../financials/statement-row-inspector.tsx | 320 +++-- 5 files changed, 1148 insertions(+), 646 deletions(-) diff --git a/app/financials/page.tsx b/app/financials/page.tsx index fb98eef..77ba663 100644 --- a/app/financials/page.tsx +++ b/app/financials/page.tsx @@ -1,10 +1,18 @@ -'use client'; +"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 { 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, @@ -13,52 +21,51 @@ import { ResponsiveContainer, Tooltip, XAxis, - YAxis -} from 'recharts'; + YAxis, +} from "recharts"; import { AlertTriangle, ChevronDown, Download, RefreshCcw, - Search -} from 'lucide-react'; -import { NormalizationSummary } from '@/components/financials/normalization-summary'; -import { StatementMatrix } from '@/components/financials/statement-matrix'; -import { StatementRowInspector } from '@/components/financials/statement-row-inspector'; -import { AppShell } from '@/components/shell/app-shell'; + Search, +} from "lucide-react"; +import { NormalizationSummary } from "@/components/financials/normalization-summary"; +import { StatementMatrix } from "@/components/financials/statement-matrix"; +import { StatementRowInspector } from "@/components/financials/statement-row-inspector"; +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'; + FinancialsToolbar, + type FinancialControlSection, +} from "@/components/financials/financials-toolbar"; +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, formatFinancialStatementValue, formatPercent, - type NumberScaleUnit -} from '@/lib/format'; -import { buildGraphingHref } from '@/lib/graphing/catalog'; -import { mergeFinancialPages } from '@/lib/financials/page-merge'; + type NumberScaleUnit, +} from "@/lib/format"; +import { buildGraphingHref } from "@/lib/graphing/catalog"; +import { mergeFinancialPages } from "@/lib/financials/page-merge"; import { buildStatementTree, resolveStatementSelection, - type StatementInspectorSelection -} from '@/lib/financials/statement-view-model'; -import { queryKeys } from '@/lib/query/keys'; -import { companyFinancialStatementsQueryOptions } from '@/lib/query/options'; + type StatementInspectorSelection, +} from "@/lib/financials/statement-view-model"; +import { queryKeys } from "@/lib/query/keys"; +import { companyFinancialStatementsQueryOptions } from "@/lib/query/options"; import type { CompanyFinancialStatementsResponse, DetailFinancialRow, DerivedFinancialRow, FinancialCadence, FinancialDisplayMode, + FinancialStatementPeriod, FinancialSurfaceKind, FinancialUnit, RatioRow, @@ -66,68 +73,73 @@ import type { StructuredKpiRow, SurfaceFinancialRow, TaxonomyStatementRow, - TrendSeries -} from '@/lib/types'; + TrendSeries, +} from "@/lib/types"; type LoadOptions = { cursor?: string | null; append?: boolean; }; -type FlatDisplayRow = TaxonomyStatementRow | StandardizedFinancialRow | RatioRow | StructuredKpiRow; +type FlatDisplayRow = + | TaxonomyStatementRow + | StandardizedFinancialRow + | RatioRow + | StructuredKpiRow; type StatementMatrixRow = SurfaceFinancialRow | DetailFinancialRow; -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 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 SURFACE_OPTIONS = [ + { value: "income_statement", label: "Income" }, + { value: "balance_sheet", label: "Balance" }, + { value: "cash_flow_statement", label: "Cash Flow" }, + { value: "ratios", label: "Ratios" }, + { value: "segments_kpis", label: "KPIs" }, ]; -const CADENCE_OPTIONS: FinancialControlOption[] = [ - { value: 'annual', label: 'Annual' }, - { value: 'quarterly', label: 'Quarterly' }, - { value: 'ltm', label: 'LTM' } +const CADENCE_OPTIONS = [ + { 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 DISPLAY_MODE_OPTIONS = [ + { value: "standardized", label: "Standard" }, + { value: "faithful", label: "Faithful" }, ]; -const CHART_MUTED = '#a1a9b3'; -const CHART_GRID = 'rgba(196, 202, 211, 0.18)'; -const CHART_TOOLTIP_BG = 'rgba(31, 34, 39, 0.96)'; -const CHART_TOOLTIP_BORDER = 'rgba(220, 226, 234, 0.24)'; +const CHART_MUTED = "#a1a9b3"; +const CHART_GRID = "rgba(196, 202, 211, 0.18)"; +const CHART_TOOLTIP_BG = "rgba(31, 34, 39, 0.96)"; +const CHART_TOOLTIP_BORDER = "rgba(220, 226, 234, 0.24)"; function formatLongDate(value: string) { const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { - return 'Unknown'; + return "Unknown"; } - return format(parsed, 'MMM dd, yyyy'); + return format(parsed, "MMM dd, yyyy"); } function isTaxonomyRow(row: FlatDisplayRow): row is TaxonomyStatementRow { - return 'localName' in row; + return "localName" in row; } function isDerivedRow(row: FlatDisplayRow): row is DerivedFinancialRow { - return 'formulaKey' in row; + return "formulaKey" in row; } function isKpiRow(row: FlatDisplayRow): row is StructuredKpiRow { - return 'provenanceType' in row; + return "provenanceType" in row; } function rowValue(row: FlatDisplayRow, periodId: string) { @@ -139,7 +151,11 @@ function statementRowValue(row: StatementMatrixRow, periodId: string) { } function isStatementSurfaceKind(surfaceKind: FinancialSurfaceKind) { - return surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement'; + return ( + surfaceKind === "income_statement" || + surfaceKind === "balance_sheet" || + surfaceKind === "cash_flow_statement" + ); } function formatMetricValue(input: { @@ -152,7 +168,7 @@ function formatMetricValue(input: { isCommonSize?: boolean; }) { if (input.value === null) { - return isStatementSurfaceKind(input.surfaceKind) ? '—' : 'n/a'; + return isStatementSurfaceKind(input.surfaceKind) ? "—" : "n/a"; } if (isStatementSurfaceKind(input.surfaceKind)) { @@ -163,19 +179,22 @@ function formatMetricValue(input: { rowKey: input.rowKey, surfaceKind: input.surfaceKind, isPercentChange: input.isPercentChange, - isCommonSize: input.isCommonSize + isCommonSize: input.isCommonSize, }); } switch (input.unit) { - case 'currency': + case "currency": return formatCurrencyByScale(input.value, input.scale); - case 'percent': + case "percent": return formatPercent(input.value * 100); - case 'shares': - case 'count': - return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2 }).format(input.value); - case 'ratio': + case "shares": + case "count": + return new Intl.NumberFormat("en-US", { + notation: "compact", + maximumFractionDigits: 2, + }).format(input.value); + case "ratio": return `${input.value.toFixed(2)}x`; default: return String(input.value); @@ -187,10 +206,10 @@ function chartTickFormatter( unit: FinancialUnit, scale: NumberScaleUnit, surfaceKind: FinancialSurfaceKind, - rowKey?: string | null + rowKey?: string | null, ) { if (!Number.isFinite(value)) { - return isStatementSurfaceKind(surfaceKind) ? '—' : 'n/a'; + return isStatementSurfaceKind(surfaceKind) ? "—" : "n/a"; } return formatMetricValue({ value, unit, scale, surfaceKind, rowKey }); @@ -212,71 +231,75 @@ function buildDisplayValue(input: { if (input.showPercentChange && input.previousPeriodId) { const previous = rowValue(input.row, input.previousPeriodId); if (current === null || previous === null || previous === 0) { - return isStatementSurfaceKind(input.surfaceKind) ? '—' : 'n/a'; + return isStatementSurfaceKind(input.surfaceKind) ? "—" : "n/a"; } return formatMetricValue({ value: (current - previous) / previous, - unit: 'percent', + unit: "percent", scale: input.scale, rowKey: input.row.key, surfaceKind: input.surfaceKind, - isPercentChange: true + isPercentChange: true, }); } if (input.showCommonSize) { - if (input.surfaceKind === 'ratios' && isDerivedRow(input.row) && input.row.unit === 'percent') { + if ( + input.surfaceKind === "ratios" && + isDerivedRow(input.row) && + input.row.unit === "percent" + ) { return formatPercent((current ?? 0) * 100); } - if (input.displayMode === 'faithful') { - return isStatementSurfaceKind(input.surfaceKind) ? '—' : 'n/a'; + if (input.displayMode === "faithful") { + return isStatementSurfaceKind(input.surfaceKind) ? "—" : "n/a"; } - const denominator = input.commonSizeRow ? rowValue(input.commonSizeRow, input.periodId) : null; + const denominator = input.commonSizeRow + ? rowValue(input.commonSizeRow, input.periodId) + : null; if (current === null || denominator === null || denominator === 0) { - return isStatementSurfaceKind(input.surfaceKind) ? '—' : 'n/a'; + return isStatementSurfaceKind(input.surfaceKind) ? "—" : "n/a"; } return formatMetricValue({ value: current / denominator, - unit: 'percent', + unit: "percent", scale: input.scale, rowKey: input.row.key, surfaceKind: input.surfaceKind, - isCommonSize: true + isCommonSize: true, }); } - const unit = isTaxonomyRow(input.row) - ? 'currency' - : input.row.unit; + const unit = isTaxonomyRow(input.row) ? "currency" : input.row.unit; return formatMetricValue({ value: current, unit, scale: input.scale, rowKey: input.row.key, - surfaceKind: input.surfaceKind + surfaceKind: input.surfaceKind, }); } function unitFromDetailRow(row: DetailFinancialRow): FinancialUnit { - const normalizedUnit = row.unit?.toLowerCase() ?? ''; + const normalizedUnit = row.unit?.toLowerCase() ?? ""; - if (normalizedUnit.includes('shares')) { - return 'shares'; + if (normalizedUnit.includes("shares")) { + return "shares"; } - if (normalizedUnit === 'pure' || normalizedUnit.includes('ratio')) { - return 'ratio'; + if (normalizedUnit === "pure" || normalizedUnit.includes("ratio")) { + return "ratio"; } - if (normalizedUnit.includes('percent')) { - return 'percent'; + if (normalizedUnit.includes("percent")) { + return "percent"; } - return 'currency'; + return "currency"; } function buildStatementTreeDisplayValue(input: { @@ -287,52 +310,61 @@ function buildStatementTreeDisplayValue(input: { showPercentChange: boolean; showCommonSize: boolean; scale: NumberScaleUnit; - surfaceKind: Extract; + surfaceKind: Extract< + FinancialSurfaceKind, + "income_statement" | "balance_sheet" | "cash_flow_statement" + >; }) { const current = statementRowValue(input.row, input.periodId); if (input.showPercentChange && input.previousPeriodId) { const previous = statementRowValue(input.row, input.previousPeriodId); if (current === null || previous === null || previous === 0) { - return '—'; + return "—"; } return formatMetricValue({ value: (current - previous) / previous, - unit: 'percent', + unit: "percent", scale: input.scale, rowKey: input.row.key, surfaceKind: input.surfaceKind, - isPercentChange: true + isPercentChange: true, }); } if (input.showCommonSize) { - const denominator = input.commonSizeRow ? statementRowValue(input.commonSizeRow, input.periodId) : null; + const denominator = input.commonSizeRow + ? statementRowValue(input.commonSizeRow, input.periodId) + : null; if (current === null || denominator === null || denominator === 0) { - return '—'; + return "—"; } return formatMetricValue({ value: current / denominator, - unit: 'percent', + unit: "percent", scale: input.scale, rowKey: input.row.key, surfaceKind: input.surfaceKind, - isCommonSize: true + isCommonSize: true, }); } return formatMetricValue({ value: current, - unit: 'conceptKey' in input.row ? unitFromDetailRow(input.row) : input.row.unit, + unit: + "conceptKey" in input.row ? unitFromDetailRow(input.row) : input.row.unit, scale: input.scale, rowKey: input.row.key, - surfaceKind: input.surfaceKind + surfaceKind: input.surfaceKind, }); } -function groupRows(rows: FlatDisplayRow[], categories: CompanyFinancialStatementsResponse['categories']) { +function groupRows( + rows: FlatDisplayRow[], + categories: CompanyFinancialStatementsResponse["categories"], +) { if (categories.length === 0) { return [{ label: null, rows }]; } @@ -340,7 +372,9 @@ function groupRows(rows: FlatDisplayRow[], categories: CompanyFinancialStatement return categories .map((category) => ({ label: category.label, - rows: rows.filter((row) => !isTaxonomyRow(row) && row.category === category.key) + rows: rows.filter( + (row) => !isTaxonomyRow(row) && row.category === category.key, + ), })) .filter((group) => group.rows.length > 0); } @@ -369,7 +403,11 @@ function ChartFrame({ children }: { children: React.ReactNode }) { return () => observer.disconnect(); }, []); - return
{ready ? children : null}
; + return ( +
+ {ready ? children : null} +
+ ); } function FinancialsPageContent() { @@ -378,26 +416,35 @@ function FinancialsPageContent() { const queryClient = useQueryClient(); const { prefetchResearchTicker } = useLinkPrefetch(); - const [tickerInput, setTickerInput] = useState('MSFT'); - const [ticker, setTicker] = useState('MSFT'); - const [surfaceKind, setSurfaceKind] = useState('income_statement'); - const [cadence, setCadence] = useState('annual'); - const [displayMode, setDisplayMode] = useState('standardized'); - const [valueScale, setValueScale] = useState('millions'); - const [rowSearch, setRowSearch] = useState(''); + const [tickerInput, setTickerInput] = useState("MSFT"); + const [ticker, setTicker] = useState("MSFT"); + const [surfaceKind, setSurfaceKind] = + useState("income_statement"); + const [cadence, setCadence] = useState("annual"); + const [displayMode, setDisplayMode] = + useState("standardized"); + const [valueScale, setValueScale] = useState("millions"); + const [rowSearch, setRowSearch] = useState(""); const [showPercentChange, setShowPercentChange] = useState(false); const [showCommonSize, setShowCommonSize] = useState(false); - const [financials, setFinancials] = useState(null); - const [selectedFlatRowKey, setSelectedFlatRowKey] = useState(null); - const [selectedRowRef, setSelectedRowRef] = useState(null); - const [expandedRowKeys, setExpandedRowKeys] = useState>(() => new Set()); + const [financials, setFinancials] = + useState(null); + const [selectedFlatRowKey, setSelectedFlatRowKey] = useState( + null, + ); + const [selectedRowRef, setSelectedRowRef] = + useState(null); + const [expandedRowKeys, setExpandedRowKeys] = useState>( + () => new Set(), + ); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [syncingFinancials, setSyncingFinancials] = useState(false); const [error, setError] = useState(null); + const [showTrendChart, setShowTrendChart] = useState(false); useEffect(() => { - const fromQuery = searchParams.get('ticker'); + const fromQuery = searchParams.get("ticker"); if (!fromQuery) { return; } @@ -415,61 +462,83 @@ function FinancialsPageContent() { }, [searchParams]); useEffect(() => { - const statementSurface = surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement'; - if (!statementSurface && displayMode !== 'standardized') { - setDisplayMode('standardized'); + const statementSurface = + surfaceKind === "income_statement" || + surfaceKind === "balance_sheet" || + surfaceKind === "cash_flow_statement"; + if (!statementSurface && displayMode !== "standardized") { + setDisplayMode("standardized"); } - if (displayMode === 'faithful') { + if (displayMode === "faithful") { setShowPercentChange(false); setShowCommonSize(false); } - if (surfaceKind === 'adjusted' || surfaceKind === 'custom_metrics') { + 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 = (selectedFlatRowKey !== null || selectedRowRef !== null) - && (surfaceKind === 'segments_kpis' || surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement'); + const loadFinancials = useCallback( + async (symbol: string, options?: LoadOptions) => { + const normalizedTicker = symbol.trim().toUpperCase(); + const nextCursor = options?.cursor ?? null; + const includeDimensions = + (selectedFlatRowKey !== null || selectedRowRef !== 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); + setLoading(true); + } else { + setLoadingMore(true); } - } finally { - setLoading(false); - setLoadingMore(false); - } - }, [cadence, queryClient, selectedFlatRowKey, selectedRowRef, surfaceKind]); + + 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, selectedFlatRowKey, selectedRowRef, surfaceKind], + ); const syncFinancials = useCallback(async () => { - const targetTicker = (financials?.company.ticker ?? ticker).trim().toUpperCase(); + const targetTicker = (financials?.company.ticker ?? ticker) + .trim() + .toUpperCase(); if (!targetTicker) { return; } @@ -479,12 +548,18 @@ function FinancialsPageContent() { 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'] }); + 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}`); + setError( + err instanceof Error + ? err.message + : `Failed to queue financial sync for ${targetTicker}`, + ); } finally { setSyncingFinancials(false); } @@ -497,11 +572,15 @@ function FinancialsPageContent() { }, [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)); + return [...(financials?.periods ?? [])].sort( + (left, right) => + Date.parse(left.periodEnd ?? left.filingDate) - + Date.parse(right.periodEnd ?? right.filingDate), + ); }, [financials?.periods]); - const isTreeStatementMode = displayMode === 'standardized' && isStatementSurfaceKind(surfaceKind); + const isTreeStatementMode = + displayMode === "standardized" && isStatementSurfaceKind(surfaceKind); const activeRows = useMemo(() => { if (!financials) { @@ -509,15 +588,15 @@ function FinancialsPageContent() { } switch (surfaceKind) { - case 'income_statement': - case 'balance_sheet': - case 'cash_flow_statement': - return displayMode === 'faithful' - ? financials.statementRows?.faithful ?? [] - : financials.statementRows?.standardized ?? []; - case 'ratios': + 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': + case "segments_kpis": return financials.kpiRows ?? []; default: return []; @@ -534,7 +613,9 @@ function FinancialsPageContent() { return activeRows; } - return activeRows.filter((row) => row.label.toLowerCase().includes(normalizedSearch)); + return activeRows.filter((row) => + row.label.toLowerCase().includes(normalizedSearch), + ); }, [activeRows, isTreeStatementMode, rowSearch]); const groupedRows = useMemo(() => { @@ -542,7 +623,11 @@ function FinancialsPageContent() { }, [filteredRows, financials?.categories]); const treeModel = useMemo(() => { - if (!isTreeStatementMode || !financials || !isStatementSurfaceKind(surfaceKind)) { + if ( + !isTreeStatementMode || + !financials || + !isStatementSurfaceKind(surfaceKind) + ) { return null; } @@ -552,9 +637,15 @@ function FinancialsPageContent() { statementDetails: financials.statementDetails, categories: financials.categories, searchQuery: rowSearch, - expandedRowKeys + expandedRowKeys, }); - }, [expandedRowKeys, financials, isTreeStatementMode, rowSearch, surfaceKind]); + }, [ + expandedRowKeys, + financials, + isTreeStatementMode, + rowSearch, + surfaceKind, + ]); const selectedRow = useMemo(() => { if (!selectedFlatRowKey) { @@ -565,7 +656,11 @@ function FinancialsPageContent() { }, [activeRows, selectedFlatRowKey]); const selectedStatementRow = useMemo(() => { - if (!isTreeStatementMode || !financials || !isStatementSurfaceKind(surfaceKind)) { + if ( + !isTreeStatementMode || + !financials || + !isStatementSurfaceKind(surfaceKind) + ) { return null; } @@ -573,7 +668,7 @@ function FinancialsPageContent() { surfaceKind, rows: financials.statementRows?.standardized ?? [], statementDetails: financials.statementDetails, - selection: selectedRowRef + selection: selectedRowRef, }); }, [financials, isTreeStatementMode, selectedRowRef, surfaceKind]); @@ -582,14 +677,18 @@ function FinancialsPageContent() { return []; } - if (selectedStatementRow?.kind === 'surface') { + if (selectedStatementRow?.kind === "surface") { return financials.dimensionBreakdown[selectedStatementRow.row.key] ?? []; } - if (selectedStatementRow?.kind === 'detail') { - return financials.dimensionBreakdown[selectedStatementRow.row.key] - ?? financials.dimensionBreakdown[selectedStatementRow.row.parentSurfaceKey] - ?? []; + if (selectedStatementRow?.kind === "detail") { + return ( + financials.dimensionBreakdown[selectedStatementRow.row.key] ?? + financials.dimensionBreakdown[ + selectedStatementRow.row.parentSurfaceKey + ] ?? + [] + ); } if (!selectedRow) { @@ -600,17 +699,20 @@ function FinancialsPageContent() { }, [financials?.dimensionBreakdown, selectedRow, selectedStatementRow]); const commonSizeRow = useMemo(() => { - if (displayMode === 'faithful' || !financials?.statementRows) { + 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 === "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; + if (surfaceKind === "balance_sheet") { + return standardizedRows.find((row) => row.key === "total_assets") ?? null; } return null; @@ -624,112 +726,73 @@ function FinancialsPageContent() { 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])) + ...Object.fromEntries( + trendSeries.map((series) => [ + series.key, + series.values[period.id] ?? null, + ]), + ), })); }, [periods, trendSeries]); const controlSections = useMemo(() => { const sections: FinancialControlSection[] = [ { - id: 'surface', - label: 'Surface', - value: surfaceKind, + key: "surface", + label: "Surface", options: SURFACE_OPTIONS, + value: surfaceKind, onChange: (value) => { setSurfaceKind(value as FinancialSurfaceKind); setSelectedFlatRowKey(null); setSelectedRowRef(null); setExpandedRowKeys(new Set()); - } + }, }, { - id: 'cadence', - label: 'Cadence', - value: cadence, + key: "cadence", + label: "Cadence", options: CADENCE_OPTIONS, + value: cadence, onChange: (value) => { setCadence(value as FinancialCadence); setSelectedFlatRowKey(null); setSelectedRowRef(null); setExpandedRowKeys(new Set()); - } - } + }, + }, ]; - if (surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement') { + if ( + surfaceKind === "income_statement" || + surfaceKind === "balance_sheet" || + surfaceKind === "cash_flow_statement" + ) { sections.push({ - id: 'display', - label: 'Display', - value: displayMode, + key: "display", + label: "Display", options: DISPLAY_MODE_OPTIONS, + value: displayMode, onChange: (value) => { setDisplayMode(value as FinancialDisplayMode); setSelectedFlatRowKey(null); setSelectedRowRef(null); setExpandedRowKeys(new Set()); - } + }, }); } sections.push({ - id: 'scale', - label: 'Scale', - value: valueScale, + key: "scale", + label: "Scale", options: FINANCIAL_VALUE_SCALE_OPTIONS, - onChange: (value) => setValueScale(value as NumberScaleUnit) + value: valueScale, + onChange: (value) => setValueScale(value as NumberScaleUnit), }); return sections; }, [cadence, displayMode, surfaceKind, valueScale]); - const controlActions = useMemo(() => { - 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]); - const toggleExpandedRow = useCallback((key: string) => { setExpandedRowKeys((current) => { const next = new Set(current); @@ -751,58 +814,76 @@ function FinancialsPageContent() { return `${filteredRows.length} of ${activeRows.length} rows`; }, [activeRows.length, filteredRows.length, treeModel]); - const valueScaleLabel = FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === valueScale)?.label ?? valueScale; + const valueScaleLabel = + FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === valueScale) + ?.label ?? valueScale; - const renderStatementTreeCellValue = useCallback((row: StatementMatrixRow, periodId: string, previousPeriodId: string | null) => { - if (!isStatementSurfaceKind(surfaceKind)) { - return '—'; - } + const renderStatementTreeCellValue = useCallback( + ( + row: StatementMatrixRow, + periodId: string, + previousPeriodId: string | null, + ) => { + if (!isStatementSurfaceKind(surfaceKind)) { + return "—"; + } - return buildStatementTreeDisplayValue({ - row, - periodId, - previousPeriodId, - commonSizeRow, - showPercentChange, - showCommonSize, - scale: valueScale, - surfaceKind - }); - }, [commonSizeRow, showCommonSize, showPercentChange, surfaceKind, valueScale]); + return buildStatementTreeDisplayValue({ + row, + periodId, + previousPeriodId, + commonSizeRow, + showPercentChange, + showCommonSize, + scale: valueScale, + surfaceKind, + }); + }, + [commonSizeRow, showCommonSize, showPercentChange, surfaceKind, valueScale], + ); - const renderDimensionValue = useCallback((value: number | null, rowKey: string, unit: FinancialUnit) => { - return formatMetricValue({ - value, - unit, - scale: valueScale, - rowKey, - surfaceKind - }); - }, [surfaceKind, valueScale]); + const renderDimensionValue = useCallback( + (value: number | null, rowKey: string, unit: FinancialUnit) => { + return formatMetricValue({ + value, + unit, + scale: valueScale, + rowKey, + surfaceKind, + }); + }, + [surfaceKind, valueScale], + ); if (isPending || !isAuthenticated) { - return
Loading financial terminal...
; + return ( +
+ Loading financial terminal... +
+ ); } return ( - )} + } > - +
{ event.preventDefault(); const normalized = tickerInput.trim().toUpperCase(); @@ -830,31 +911,42 @@ function FinancialsPageContent() { > setTickerInput(event.target.value.toUpperCase())} + onChange={(event) => + setTickerInput(event.target.value.toUpperCase()) + } placeholder="Ticker (AAPL)" className="w-full sm:max-w-xs" + inputSize="compact" /> - {financials ? ( <> prefetchResearchTicker(financials.company.ticker)} - onFocus={() => prefetchResearchTicker(financials.company.ticker)} - className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" + onMouseEnter={() => + prefetchResearchTicker(financials.company.ticker) + } + onFocus={() => + prefetchResearchTicker(financials.company.ticker) + } + className="text-xs text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > - Open overview + Overview prefetchResearchTicker(financials.company.ticker)} - onFocus={() => prefetchResearchTicker(financials.company.ticker)} - className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" + onMouseEnter={() => + prefetchResearchTicker(financials.company.ticker) + } + onFocus={() => + prefetchResearchTicker(financials.company.ticker) + } + className="text-xs text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > - Open graphing + Graphing ) : null} @@ -862,113 +954,142 @@ function FinancialsPageContent() { {error ? ( - +

{error}

) : null} - - - -
- setRowSearch(event.target.value)} - placeholder="Search rows by label" - className="w-full sm:max-w-sm" - /> - - {rowResultCountLabel} - + + { + console.log("Export clicked"); + }} + showChart={showTrendChart} + onToggleChart={() => setShowTrendChart(!showTrendChart)} + /> +
+ {rowResultCountLabel}
- - {loading ? ( -

Loading trend chart...

- ) : chartData.length === 0 || trendSeries.length === 0 ? ( -

No trend data available for the selected surface.

- ) : ( - - - - - - chartTickFormatter( - value, - trendSeries[0]?.unit ?? 'currency', - valueScale, - surfaceKind, - trendSeries[0]?.key ?? null - )} - /> - { - const numeric = Number(value); - const series = trendSeries.find((candidate) => candidate.key === entry.dataKey); - return formatMetricValue({ - value: Number.isFinite(numeric) ? numeric : null, - unit: series?.unit ?? 'currency', - scale: valueScale, - rowKey: series?.key ?? null, - surfaceKind - }); - }} - contentStyle={{ - backgroundColor: CHART_TOOLTIP_BG, - border: `1px solid ${CHART_TOOLTIP_BORDER}`, - borderRadius: '0.75rem' - }} - /> - {trendSeries.map((series, index) => ( - +
+

+ Trend Chart +

+
+ {loading ? ( +

+ Loading trend chart... +

+ ) : chartData.length === 0 || trendSeries.length === 0 ? ( +

+ No trend data available for the selected surface. +

+ ) : ( + + + + + - ))} - - - - )} -
- - {financials && isTreeStatementMode ? ( - + + chartTickFormatter( + value, + trendSeries[0]?.unit ?? "currency", + valueScale, + surfaceKind, + trendSeries[0]?.key ?? null, + ) + } + /> + { + const numeric = Number(value); + const series = trendSeries.find( + (candidate) => candidate.key === entry.dataKey, + ); + return formatMetricValue({ + value: Number.isFinite(numeric) ? numeric : null, + unit: series?.unit ?? "currency", + scale: valueScale, + rowKey: series?.key ?? null, + surfaceKind, + }); + }} + contentStyle={{ + backgroundColor: CHART_TOOLTIP_BG, + border: `1px solid ${CHART_TOOLTIP_BORDER}`, + borderRadius: "0.75rem", + }} + /> + {trendSeries.map((series, index) => ( + + ))} + + + + )} + ) : null} - + {loading ? ( -

Loading financial matrix...

- ) : surfaceKind === 'adjusted' || surfaceKind === 'custom_metrics' ? ( +

+ Loading financial matrix... +

+ ) : surfaceKind === "adjusted" || surfaceKind === "custom_metrics" ? (
-

This surface is not yet available in v1.

-

Adjusted and custom metrics are API-visible placeholders only. No edits or derived rows are available yet.

+

+ This surface is not yet available in v1. +

+

+ Adjusted and custom metrics are API-visible placeholders only. No + edits or derived rows are available yet. +

- ) : periods.length === 0 || (isTreeStatementMode ? (treeModel?.visibleNodeCount ?? 0) === 0 : filteredRows.length === 0) ? ( -

No rows available for the selected filters yet.

+ ) : periods.length === 0 || + (isTreeStatementMode + ? (treeModel?.visibleNodeCount ?? 0) === 0 + : filteredRows.length === 0) ? ( +

+ No rows available for the selected filters yet. +

) : ( -
+
{isStatementSurfaceKind(surfaceKind) ? ( -
+

USD · {valueScaleLabel}

{isTreeStatementMode && hasUnmappedResidualRows ? (

- Parser residual rows are available under the Unmapped / Residual section. + Parser residual rows are available under the{" "} + + Unmapped / Residual + {" "} + section.

) : null}
@@ -982,18 +1103,28 @@ function FinancialsPageContent() { onSelectRow={setSelectedRowRef} renderCellValue={renderStatementTreeCellValue} periodLabelFormatter={formatLongDate} + dense={true} + virtualized={treeModel.visibleNodeCount > 50} /> ) : (
- + {periods.map((period) => ( ))} @@ -1001,29 +1132,51 @@ function FinancialsPageContent() { {groupedRows.map((group) => ( - + {group.label ? ( - + ) : null} {group.rows.map((row) => ( setSelectedFlatRowKey(row.key)} > @@ -1032,13 +1185,16 @@ function FinancialsPageContent() { {buildDisplayValue({ row, periodId: period.id, - previousPeriodId: index > 0 ? periods[index - 1]?.id ?? null : null, + previousPeriodId: + index > 0 + ? (periods[index - 1]?.id ?? null) + : null, commonSizeRow, displayMode, showPercentChange, showCommonSize, scale: valueScale, - surfaceKind + surfaceKind, })} ))} @@ -1064,55 +1220,89 @@ function FinancialsPageContent() { renderDimensionValue={renderDimensionValue} /> ) : ( - + {!selectedRow ? ( -

Select a row to inspect details.

+

+ Select a row to inspect details. +

) : (
-
-
+
+

Label

-

{selectedRow.label}

+

+ {selectedRow.label} +

-
+

Key

-

{selectedRow.key}

+

+ {selectedRow.key} +

{isTaxonomyRow(selectedRow) ? ( -
-

Taxonomy Concept

-

{selectedRow.qname}

+
+

+ Taxonomy Concept +

+

+ {selectedRow.qname} +

) : ( -
-
-

Category

-

{selectedRow.category}

+
+
+

+ Category +

+

+ {selectedRow.category} +

-
+

Unit

-

{selectedRow.unit}

+

+ {selectedRow.unit} +

)} {isDerivedRow(selectedRow) ? ( -
-

Source Row Keys

-

{(selectedRow.sourceRowKeys ?? []).join(', ') || 'n/a'}

-

Source Concepts

-

{(selectedRow.sourceConcepts ?? []).join(', ') || 'n/a'}

-

Source Fact IDs

-

{(selectedRow.sourceFactIds ?? []).join(', ') || 'n/a'}

+
+

+ Source Row Keys +

+

+ {(selectedRow.sourceRowKeys ?? []).join(", ") || "n/a"} +

+

+ Source Concepts +

+

+ {(selectedRow.sourceConcepts ?? []).join(", ") || "n/a"} +

+

+ Source Fact IDs +

+

+ {(selectedRow.sourceFactIds ?? []).join(", ") || "n/a"} +

) : null} - {!selectedRow || !('hasDimensions' in selectedRow) || !selectedRow.hasDimensions ? ( -

No dimensional drill-down is available for this row.

+ {!selectedRow || + !("hasDimensions" in selectedRow) || + !selectedRow.hasDimensions ? ( +

+ No dimensional drill-down is available for this row. +

) : dimensionRows.length === 0 ? ( -

No dimensional facts were returned for the selected row.

+

+ No dimensional facts were returned for the selected row. +

) : (
Metric + Metric +
- {formatLongDate(period.periodEnd ?? period.filingDate)} - {period.filingType} · {period.periodLabel} + + {formatLongDate( + period.periodEnd ?? period.filingDate, + )} + + + {period.filingType} · {period.periodLabel} +
{group.label} + {group.label} +
- {row.label} - {'hasDimensions' in row && row.hasDimensions ? : null} + + {row.label} + + {"hasDimensions" in row && + row.hasDimensions ? ( + + ) : null}
{isDerivedRow(row) && row.formulaKey ? ( - Formula: {row.formulaKey} + + Formula: {row.formulaKey} + ) : null} {isKpiRow(row) ? ( - Provenance: {row.provenanceType} + + Provenance: {row.provenanceType} + ) : null}
@@ -1126,17 +1316,27 @@ function FinancialsPageContent() { {dimensionRows.map((row, index) => ( - - + + - + ))} @@ -1148,14 +1348,22 @@ function FinancialsPageContent() { )} - {(surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement') && financials ? ( - + {(surfaceKind === "income_statement" || + surfaceKind === "balance_sheet" || + surfaceKind === "cash_flow_statement") && + financials ? ( +
- Overall status: {financials.metrics.validation?.status ?? 'not_run'} + + Overall status:{" "} + {financials.metrics.validation?.status ?? "not_run"} +
{(financials.metrics.validation?.checks.length ?? 0) === 0 ? ( -

No validation checks available yet.

+

+ No validation checks available yet. +

) : (
{periods.find((period) => period.id === row.periodId)?.periodLabel ?? row.periodId}
+ {periods.find( + (period) => period.id === row.periodId, + )?.periodLabel ?? row.periodId} + {row.axis} {row.member}{formatMetricValue({ - value: row.value, - unit: isTaxonomyRow(selectedRow) ? 'currency' : selectedRow.unit, - scale: valueScale, - rowKey: selectedRow.key, - surfaceKind - })} + {formatMetricValue({ + value: row.value, + unit: isTaxonomyRow(selectedRow) + ? "currency" + : selectedRow.unit, + scale: valueScale, + rowKey: selectedRow.key, + surfaceKind, + })} +
@@ -1172,22 +1380,26 @@ function FinancialsPageContent() { {financials.metrics.validation?.checks.map((check) => ( - - + + - + ))} @@ -1196,13 +1408,23 @@ function FinancialsPageContent() { )} ) : null} + + {financials && isTreeStatementMode ? ( + + ) : null} ); } export default function FinancialsPage() { return ( - Loading financial terminal...}> + + Loading financial terminal... + + } + > ); diff --git a/components/dashboard/index-card-row.tsx b/components/dashboard/index-card-row.tsx index def273b..0556fd6 100644 --- a/components/dashboard/index-card-row.tsx +++ b/components/dashboard/index-card-row.tsx @@ -10,6 +10,8 @@ type IndexCardProps = { positive?: boolean; }; +export type { IndexCardProps }; + type IndexCardRowProps = { cards: IndexCardProps[]; className?: string; diff --git a/components/financials/financials-toolbar.tsx b/components/financials/financials-toolbar.tsx index ac3e63e..1429291 100644 --- a/components/financials/financials-toolbar.tsx +++ b/components/financials/financials-toolbar.tsx @@ -1,10 +1,18 @@ -'use client'; +"use client"; -import { memo, useMemo, useCallback, useRef, useEffect, useState } from 'react'; -import { Download, Search } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { cn } from '@/lib/utils'; +import { + Fragment, + memo, + useMemo, + useCallback, + useRef, + useEffect, + useState, +} from "react"; +import { Download, Search, BarChart3 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; export type FinancialControlOption = { value: string; @@ -24,10 +32,11 @@ export type FinancialsToolbarProps = { searchValue: string; onSearchChange: (value: string) => void; onExport?: () => void; + showChart?: boolean; + onToggleChart?: () => void; className?: string; }; -// Debounce hook function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); @@ -44,76 +53,185 @@ function useDebounce(value: T, delay: number): T { return debouncedValue; } +function ControlGroup({ + children, + showDivider = false, +}: { + children: React.ReactNode; + showDivider?: boolean; +}) { + return ( +
+ {showDivider &&
} + {children} +
+ ); +} + function FinancialsToolbarComponent({ sections, searchValue, onSearchChange, onExport, - className + showChart = false, + onToggleChart, + className, }: FinancialsToolbarProps) { const [localSearch, setLocalSearch] = useState(searchValue); const debouncedSearch = useDebounce(localSearch, 300); - // Sync debounced search to parent useEffect(() => { onSearchChange(debouncedSearch); }, [debouncedSearch, onSearchChange]); - // Sync parent search to local (if changed externally) useEffect(() => { setLocalSearch(searchValue); }, [searchValue]); + const groupedSections = useMemo(() => { + const statementKeys = ["surface"]; + const periodKeys = ["cadence"]; + const modeKeys = ["display"]; + const scaleKeys = ["scale"]; + + return { + statement: sections.filter((s) => statementKeys.includes(s.key)), + period: sections.filter((s) => periodKeys.includes(s.key)), + mode: sections.filter((s) => modeKeys.includes(s.key)), + scale: sections.filter((s) => scaleKeys.includes(s.key)), + }; + }, [sections]); + return ( -
- {/* Control sections */} -
- {sections.map((section) => ( -
- {section.options.map((option) => { - const isActive = section.value === option.value; - return ( - - ); - })} -
- ))} -
+
+
+ + {groupedSections.statement.map((section) => ( + + {section.options.map((option) => { + const isActive = section.value === option.value; + return ( + + ); + })} + + ))} + - {/* Spacer */} -
+ + {groupedSections.period.map((section) => ( + + {section.options.map((option) => { + const isActive = section.value === option.value; + return ( + + ); + })} + + ))} + - {/* Search and actions */} -
-
- - setLocalSearch(e.target.value)} - inputSize="compact" - className="w-48 pl-7" - /> -
- {onExport && ( - + {groupedSections.mode.length > 0 && ( + + {groupedSections.mode.map((section) => ( + + {section.options.map((option) => { + const isActive = section.value === option.value; + return ( + + ); + })} + + ))} + )} + + + {groupedSections.scale.map((section) => ( + + {section.options.map((option) => { + const isActive = section.value === option.value; + return ( + + ); + })} + + ))} + + +
+ +
+
+ + setLocalSearch(e.target.value)} + inputSize="compact" + className="w-40 pl-7" + /> +
+ + {onToggleChart && ( + + )} + + {onExport && ( + + )} +
); diff --git a/components/financials/normalization-summary.tsx b/components/financials/normalization-summary.tsx index 964f3da..f7af55e 100644 --- a/components/financials/normalization-summary.tsx +++ b/components/financials/normalization-summary.tsx @@ -1,66 +1,103 @@ -'use client'; +"use client"; -import { AlertTriangle } from 'lucide-react'; -import { Panel } from '@/components/ui/panel'; -import type { NormalizationMetadata } from '@/lib/types'; -import { cn } from '@/lib/utils'; +import { AlertTriangle } from "lucide-react"; +import type { NormalizationMetadata } from "@/lib/types"; +import { cn } from "@/lib/utils"; type NormalizationSummaryProps = { normalization: NormalizationMetadata; }; -function SummaryCard(props: { +function SummaryField(props: { label: string; value: string; - tone?: 'default' | 'warning'; + tone?: "default" | "warning"; }) { return (
-

{props.label}

-

{props.value}

+

+ {props.label} +

+

+ {props.value} +

); } -export function NormalizationSummary({ normalization }: NormalizationSummaryProps) { +export function NormalizationSummary({ + normalization, +}: NormalizationSummaryProps) { const hasMaterialUnmapped = normalization.materialUnmappedRowCount > 0; const hasWarnings = normalization.warnings.length > 0; return ( - -
- - - - - - - - +
+

+ Normalization Summary +

+

+ Pack, parser, and residual mapping health for the compact statement + surface. +

+
+ +
+ + + + + + + +
+ {hasWarnings ? ( -
-

+

+

Parser Warnings

{normalization.warnings.map((warning) => ( {warning} @@ -68,12 +105,17 @@ export function NormalizationSummary({ normalization }: NormalizationSummaryProp
) : null} + {hasMaterialUnmapped ? ( -
+
-

Material unmapped rows were detected for this filing set. Use the inspector and detail rows before relying on cross-company comparisons.

+

+ Material unmapped rows were detected for this filing set. Use the + inspector and detail rows before relying on cross-company + comparisons. +

) : null} - + ); } diff --git a/components/financials/statement-row-inspector.tsx b/components/financials/statement-row-inspector.tsx index bc80ab9..db8075a 100644 --- a/components/financials/statement-row-inspector.tsx +++ b/components/financials/statement-row-inspector.tsx @@ -1,86 +1,145 @@ -'use client'; +"use client"; -import { Panel } from '@/components/ui/panel'; import type { DetailFinancialRow, DimensionBreakdownRow, FinancialStatementPeriod, FinancialSurfaceKind, - SurfaceFinancialRow -} from '@/lib/types'; -import type { ResolvedStatementSelection } from '@/lib/financials/statement-view-model'; + SurfaceFinancialRow, +} from "@/lib/types"; +import type { ResolvedStatementSelection } from "@/lib/financials/statement-view-model"; type StatementRowInspectorProps = { selection: ResolvedStatementSelection | null; dimensionRows: DimensionBreakdownRow[]; periods: FinancialStatementPeriod[]; - surfaceKind: Extract; - renderValue: (row: SurfaceFinancialRow | DetailFinancialRow, periodId: string, previousPeriodId: string | null) => string; - renderDimensionValue: (value: number | null, rowKey: string, unit: SurfaceFinancialRow['unit']) => string; + surfaceKind: Extract< + FinancialSurfaceKind, + "income_statement" | "balance_sheet" | "cash_flow_statement" + >; + renderValue: ( + row: SurfaceFinancialRow | DetailFinancialRow, + periodId: string, + previousPeriodId: string | null, + ) => string; + renderDimensionValue: ( + value: number | null, + rowKey: string, + unit: SurfaceFinancialRow["unit"], + ) => string; }; -function InspectorCard(props: { - label: string; - value: string; -}) { +function InspectorField(props: { label: string; value: string }) { return ( -
-

{props.label}

-

{props.value}

+
+

+ {props.label} +

+

+ {props.value} +

); } function renderList(values: string[] | null | undefined) { - return (values ?? []).length > 0 ? (values ?? []).join(', ') : 'n/a'; + return (values ?? []).length > 0 ? (values ?? []).join(", ") : "n/a"; } export function StatementRowInspector(props: StatementRowInspectorProps) { const selection = props.selection; - const parentSurfaceLabel = selection?.kind === 'detail' - ? selection.parentSurfaceRow?.label ?? (selection.row.parentSurfaceKey === 'unmapped' ? 'Unmapped / Residual' : selection.row.parentSurfaceKey) - : null; + const parentSurfaceLabel = + selection?.kind === "detail" + ? (selection.parentSurfaceRow?.label ?? + (selection.row.parentSurfaceKey === "unmapped" + ? "Unmapped / Residual" + : selection.row.parentSurfaceKey)) + : null; return ( - +
+
+

+ Row Details +

+

+ Inspect compact-surface resolution, raw drill-down rows, and + dimensional evidence. +

+
+ {!selection ? ( -

Select a compact surface row or raw detail row to inspect details.

- ) : selection.kind === 'surface' ? ( +

+ Select a compact surface row or raw detail row to inspect details. +

+ ) : selection.kind === "surface" ? (
-
- - - - +
+ + + +
-
- - +
+ +
-
- 0 ? (selection.row.sourceFactIds ?? []).join(', ') : 'n/a'} /> - +
+ 0 + ? (selection.row.sourceFactIds ?? []).join(", ") + : "n/a" + } + /> +
-
- 0 ? selection.childSurfaceRows.map((row) => row.label).join(', ') : 'None'} /> - +
+ 0 + ? selection.childSurfaceRows + .map((row) => row.label) + .join(", ") + : "None" + } + /> +
{selection.detailRows.length > 0 ? ( -
-

Raw Detail Labels

+
+

+ Raw Detail Labels +

{selection.detailRows.map((row) => ( {row.label} @@ -91,8 +150,106 @@ export function StatementRowInspector(props: StatementRowInspectorProps) { {selection.row.hasDimensions ? ( props.dimensionRows.length === 0 ? ( -

No dimensional facts were returned for the selected compact row.

+

+ No dimensional facts were returned for the selected compact row. +

) : ( +
+
+
{check.metricKey}{formatMetricValue({ - value: check.taxonomyValue, - unit: 'currency', - scale: valueScale, - rowKey: check.metricKey, - surfaceKind - })}{formatMetricValue({ - value: check.llmValue, - unit: 'currency', - scale: valueScale, - rowKey: check.metricKey, - surfaceKind - })} + {formatMetricValue({ + value: check.taxonomyValue, + unit: "currency", + scale: valueScale, + rowKey: check.metricKey, + surfaceKind, + })} + + {formatMetricValue({ + value: check.llmValue, + unit: "currency", + scale: valueScale, + rowKey: check.metricKey, + surfaceKind, + })} + {check.status}{(check.evidencePages ?? []).join(', ') || 'n/a'}{(check.evidencePages ?? []).join(", ") || "n/a"}
+ + + + + + + + + + {props.dimensionRows.map((row, index) => ( + + + + + + + ))} + +
PeriodAxisMemberValue
+ {props.periods.find( + (period) => period.id === row.periodId, + )?.periodLabel ?? row.periodId} + {row.axis}{row.member} + {props.renderDimensionValue( + row.value, + selection.row.key, + selection.row.unit, + )} +
+
+
+ ) + ) : ( +

+ No dimensional drill-down is available for this compact surface + row. +

+ )} +
+ ) : ( +
+
+ + + + +
+ +
+ + +
+ +
+ + 0 + ? (selection.row.sourceFactIds ?? []).join(", ") + : "n/a" + } + /> +
+ +
+ +
+ + {props.dimensionRows.length === 0 ? ( +

+ No dimensional facts were returned for the selected raw detail + row. +

+ ) : ( +
@@ -105,73 +262,34 @@ export function StatementRowInspector(props: StatementRowInspectorProps) { {props.dimensionRows.map((row, index) => ( - - + + - + ))}
{props.periods.find((period) => period.id === row.periodId)?.periodLabel ?? row.periodId}
+ {props.periods.find( + (period) => period.id === row.periodId, + )?.periodLabel ?? row.periodId} + {row.axis} {row.member}{props.renderDimensionValue(row.value, selection.row.key, selection.row.unit)} + {props.renderDimensionValue( + row.value, + selection.row.key, + props.surfaceKind === "balance_sheet" + ? "currency" + : "currency", + )} +
- ) - ) : ( -

No dimensional drill-down is available for this compact surface row.

- )} -
- ) : ( -
-
- - - - -
- -
- - -
- -
- - 0 ? (selection.row.sourceFactIds ?? []).join(', ') : 'n/a'} /> -
- -
-

Dimensions Summary

-

{renderList(selection.row.dimensionsSummary)}

-
- - {props.dimensionRows.length === 0 ? ( -

No dimensional facts were returned for the selected raw detail row.

- ) : ( -
- - - - - - - - - - - {props.dimensionRows.map((row, index) => ( - - - - - - - ))} - -
PeriodAxisMemberValue
{props.periods.find((period) => period.id === row.periodId)?.periodLabel ?? row.periodId}{row.axis}{row.member}{props.renderDimensionValue(row.value, selection.row.key, props.surfaceKind === 'balance_sheet' ? 'currency' : 'currency')}
)}
)} - + ); }