import { Box, Text, useUiCapabilities } from "../../../../ui";
import { TextAttributes, type ScrollBoxRenderable } from "../../../../ui";
import { useShortcut } from "../../../../react/input";
import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from "react";
import { usePaneStateValue, usePaneTicker } from "../../../../state/app/context";
import {
DataTableView,
DisclosureMarker,
PaneStatusBody,
QueryBar,
usePaneFooter,
usePaneNoticeFooter,
type DataTableCell,
type DataTableColumn,
} from "../../../../components";
import { colors, priceColor } from "../../../../theme/colors";
import { padTo } from "../../../../utils/format";
import {
FINANCIAL_COL_W,
FINANCIAL_LABEL_W,
FINANCIAL_SUB_TABS,
buildFinancialRows,
canCompareFinancialRow,
collectDefaultCollapsedGroupIds,
collectGroupIds,
computeGrowth,
formatFinancialCell,
formatFinancialHeader,
financialStatementCurrency,
financialStatementLimitations,
formatFinancialValue,
resolveFinancialPeriod,
resolveFinancialPeriodOption,
resolveFinancialSubTabKey,
semanticGrowthValue,
selectFinancialStatements,
statementMetricValue,
type FinancialPeriod,
type FinancialTableRow,
} from "./model";
import type { FinancialTableStatement } from "./aggregation";
type FinancialTableColumn = DataTableColumn & (
| { id: "metric"; kind: "metric" }
| { id: string; kind: "statement"; statement: FinancialTableStatement }
);
export function FinancialsTab({
width,
focused,
headerScrollId,
bodyScrollId,
allowArrowSubTabNavigation = true,
}: {
width: number;
focused: boolean;
headerScrollId?: string;
bodyScrollId?: string;
allowArrowSubTabNavigation?: boolean;
}) {
const { financials } = usePaneTicker();
return (
);
}
const financialRowKey = (row: FinancialTableRow) => row.id;
const financialRowBackground = (row: FinancialTableRow) => (
row.kind === "group" && row.depth === 0 ? colors.panel : undefined
);
/** The compact header, plus the column's currency when the table has no single one. */
function financialColumnHeader(statement: FinancialTableStatement, sharedCurrency: string | undefined): string {
const header = formatFinancialHeader(statement.date, undefined, statement.dateSource, true, statement.aggregation?.periodEnd);
return !sharedCurrency && statement.currency ? `${header} ${statement.currency}` : header;
}
export function ResolvedFinancialsTab({
width,
focused,
financials,
headerScrollId,
bodyScrollId,
allowArrowSubTabNavigation = true,
}: {
width: number;
focused: boolean;
financials: ReturnType["financials"];
headerScrollId?: string;
bodyScrollId?: string;
allowArrowSubTabNavigation?: boolean;
}) {
// Keyed on the snapshot so the derived statement lists, columns, and rows
// below keep their identity across renders that only move the selection;
// the table memoizes rows on exactly those references.
const annualStatements = useMemo(
() => [...(financials?.annualStatements ?? [])].sort((a, b) => a.date.localeCompare(b.date)),
[financials],
);
const quarterlyStatements = useMemo(
() => [...(financials?.quarterlyStatements ?? [])].sort((a, b) => a.date.localeCompare(b.date)),
[financials],
);
const comparisonCurrency = financialStatementCurrency(financials, [...annualStatements, ...quarterlyStatements]);
const hasAnnualStatements = annualStatements.length > 0;
const hasQuarterlyStatements = quarterlyStatements.length > 0;
const fallbackPeriod: FinancialPeriod = hasAnnualStatements ? "annual" : "quarterly";
const [storedPeriod, setStoredPeriod] = usePaneStateValue("financialPeriod", fallbackPeriod);
const period = resolveFinancialPeriodOption(storedPeriod) ?? fallbackPeriod;
const [storedSubTab, setStoredSubTab] = usePaneStateValue("financialSubTab", FINANCIAL_SUB_TABS[0]!.key);
const resolvedSubTabKey = resolveFinancialSubTabKey(storedSubTab);
const subTabIdx = Math.max(0, FINANCIAL_SUB_TABS.findIndex((tab) => tab.key === resolvedSubTabKey));
const [collapsedGroups, setCollapsedGroups] = useState>(
() => new Set(collectDefaultCollapsedGroupIds(FINANCIAL_SUB_TABS.flatMap((tab) => tab.rows))),
);
const [selectedRowId, setSelectedRowId] = useState(null);
const bodyScrollRef = useRef(null);
const headerScrollRef = useRef(null);
const { nativePaneChrome } = useUiCapabilities();
const subTab = FINANCIAL_SUB_TABS[subTabIdx]!;
const currentGroupIds = useMemo(() => collectGroupIds(subTab.rows), [subTab]);
const hasCollapsedCurrentGroup = currentGroupIds.some((id) => collapsedGroups.has(id));
const hasExpandedCurrentGroup = currentGroupIds.some((id) => !collapsedGroups.has(id));
const setPeriod = useCallback((next: SetStateAction) => {
setStoredPeriod(next);
}, [setStoredPeriod]);
const setSubTabIdx = useCallback((next: SetStateAction) => {
setStoredSubTab((currentKey) => {
const currentSubTabKey = resolveFinancialSubTabKey(currentKey);
const currentIndex = Math.max(0, FINANCIAL_SUB_TABS.findIndex((tab) => tab.key === currentSubTabKey));
const rawIndex = typeof next === "function" ? next(currentIndex) : next;
const boundedIndex = ((rawIndex % FINANCIAL_SUB_TABS.length) + FINANCIAL_SUB_TABS.length) % FINANCIAL_SUB_TABS.length;
return FINANCIAL_SUB_TABS[boundedIndex]?.key ?? FINANCIAL_SUB_TABS[0]!.key;
});
}, [setStoredSubTab]);
const selectAdjacentSubTab = useCallback((direction: -1 | 1) => {
setSubTabIdx((current) => current + direction);
}, [setSubTabIdx]);
const togglePeriod = useCallback(() => {
if (!hasAnnualStatements && !hasQuarterlyStatements) return;
setPeriod((current) => {
const resolved = resolveFinancialPeriod(current, hasAnnualStatements, hasQuarterlyStatements);
if (resolved === "annual" && hasQuarterlyStatements) return "quarterly";
if (hasAnnualStatements) return "annual";
return "quarterly";
});
}, [hasAnnualStatements, hasQuarterlyStatements, setPeriod]);
const toggleGroup = useCallback((groupId: string) => {
setCollapsedGroups((current) => {
const next = new Set(current);
if (next.has(groupId)) next.delete(groupId);
else next.add(groupId);
return next;
});
}, []);
const expandCurrentGroups = useCallback(() => {
setCollapsedGroups((current) => {
const next = new Set(current);
for (const groupId of currentGroupIds) next.delete(groupId);
return next;
});
}, [currentGroupIds]);
const collapseCurrentGroups = useCallback(() => {
setCollapsedGroups((current) => {
const next = new Set(current);
for (const groupId of currentGroupIds) next.add(groupId);
return next;
});
}, [currentGroupIds]);
usePaneNoticeFooter({
registrationId: "financials-notices",
notices: financialStatementLimitations(financials),
focused,
});
// The active section and period are already the visible tab selections, so the
// footer only carries what the controls cannot show.
usePaneFooter("financials", () => ({
info: [],
hints: [
{
id: "section",
key: "1-3",
label: "section",
disabled: !financials,
onPress: () => setSubTabIdx((current) => (current + 1) % FINANCIAL_SUB_TABS.length),
},
{
id: "period",
key: "p",
label: "eriod",
disabled: !hasAnnualStatements && !hasQuarterlyStatements,
onPress: togglePeriod,
},
{
id: "expand-groups",
key: "e",
label: "xpand",
disabled: !hasCollapsedCurrentGroup,
onPress: expandCurrentGroups,
},
{
id: "collapse-groups",
key: "c",
label: "ollapse",
disabled: !hasExpandedCurrentGroup,
onPress: collapseCurrentGroups,
},
],
}), [
collapseCurrentGroups,
expandCurrentGroups,
financials,
hasAnnualStatements,
hasCollapsedCurrentGroup,
hasExpandedCurrentGroup,
hasQuarterlyStatements,
setSubTabIdx,
togglePeriod,
]);
const syncHeaderScroll = useCallback(() => {
const body = bodyScrollRef.current;
const header = headerScrollRef.current;
if (body && header && header.scrollLeft !== body.scrollLeft) {
header.scrollLeft = body.scrollLeft;
}
}, []);
useShortcut((event) => {
if (!focused) return;
if (event.ctrl || event.meta || event.alt || event.super || event.targetEditable) return;
const keyName = event.name || event.key || event.sequence;
if (keyName === "p") {
event.preventDefault();
event.stopPropagation();
togglePeriod();
} else if (keyName === "e" && hasCollapsedCurrentGroup) {
event.preventDefault();
event.stopPropagation();
expandCurrentGroups();
} else if (keyName === "c" && hasExpandedCurrentGroup) {
event.preventDefault();
event.stopPropagation();
collapseCurrentGroups();
} else if (keyName === "1" || keyName === "2" || keyName === "3") {
event.preventDefault();
event.stopPropagation();
setSubTabIdx(Number(keyName) - 1);
} else if (allowArrowSubTabNavigation && keyName === "left") {
event.preventDefault();
event.stopPropagation();
selectAdjacentSubTab(-1);
} else if (allowArrowSubTabNavigation && keyName === "right") {
event.preventDefault();
event.stopPropagation();
selectAdjacentSubTab(1);
}
}, { phase: "before" });
useEffect(() => {
if (period === "annual" && !hasAnnualStatements && hasQuarterlyStatements) {
setPeriod("quarterly");
} else if (period === "quarterly" && !hasQuarterlyStatements && hasAnnualStatements) {
setPeriod("annual");
}
}, [hasAnnualStatements, hasQuarterlyStatements, period, setPeriod]);
const resolvedPeriod = resolveFinancialPeriod(period, hasAnnualStatements, hasQuarterlyStatements);
const isAnnual = resolvedPeriod === "annual";
const { statements: displayStatements, previousStatementMap } = useMemo(
() => selectFinancialStatements(resolvedPeriod, subTab.key, annualStatements, quarterlyStatements),
[annualStatements, quarterlyStatements, resolvedPeriod, subTab.key],
);
// One currency for every column goes in the query bar with the growth basis;
// a column in a different currency (or none) says so in its own header.
const growthBasis = isAnnual ? "YoY" : "QoQ";
const columns = useMemo(() => [
{
id: "metric",
kind: "metric",
label: "Metric",
width: FINANCIAL_LABEL_W,
align: "left",
},
...displayStatements.map((statement, index): FinancialTableColumn => ({
id: `statement:${statement.date}:${index}`,
kind: "statement",
statement,
label: padTo(financialColumnHeader(statement, comparisonCurrency), FINANCIAL_COL_W, "center"),
width: FINANCIAL_COL_W,
align: "right",
headerColor: statement.date === "TTM" ? colors.textBright : colors.textDim,
})),
], [comparisonCurrency, displayStatements, isAnnual]);
const rows = useMemo(
() => buildFinancialRows(subTab.rows, displayStatements, collapsedGroups),
[collapsedGroups, displayStatements, subTab.rows],
);
const renderCell = useCallback((
row: FinancialTableRow,
column: FinancialTableColumn,
): DataTableCell => {
if (column.kind === "metric") {
if (row.kind === "group") {
const indent = " ".repeat(row.depth * 2);
const marker = row.toggleable ? (row.expanded ? "\u25be" : "\u25b8") : " ";
const labelColor = row.depth === 0 ? colors.textBright : colors.textDim;
const labelAttributes = row.depth === 0 ? TextAttributes.BOLD : TextAttributes.NONE;
return {
text: `${indent}${marker} ${row.unitLabel}`,
color: labelColor,
attributes: labelAttributes,
// The desktop draws the disclosure as a path, not a glyph.
content: (
{indent}
{row.toggleable ? : }
{row.unitLabel}
),
onMouseDown: row.toggleable
? (event) => {
event.preventDefault?.();
event.stopPropagation?.();
toggleGroup(row.id);
}
: undefined,
};
}
return {
text: `${" ".repeat(row.depth * 2 + 2)}${row.unitLabel}`,
color: colors.textDim,
};
}
const key = row.kind === "group" ? row.summaryKey : undefined;
if (row.kind === "group" && !key) return {
text: "",
};
const previous = previousStatementMap.get(column.statement.date);
const value = row.kind === "group"
? column.statement[key!] as number | undefined
: statementMetricValue(row, column.statement);
const previousValue = previous
? row.kind === "group"
? previous[key!] as number | undefined
: statementMetricValue(row, previous)
: undefined;
const growth = (row.kind === "metric" && !row.showGrowth)
|| !canCompareFinancialRow(row, column.statement, previous, comparisonCurrency)
? undefined : computeGrowth(value, previousValue);
const formattedValue = formatFinancialValue(value, row);
const cell = formatFinancialCell(formattedValue, growth);
const growthColorValue = semanticGrowthValue(growth, row.growthDirection);
return {
text: `${cell.valueText}${cell.growthText}`,
content: (
{cell.valueText}
{cell.growthText}
),
};
}, [comparisonCurrency, previousStatementMap, toggleGroup]);
useEffect(() => {
if (rows.length === 0) {
if (selectedRowId !== null) setSelectedRowId(null);
return;
}
if (selectedRowId && rows.some((row) => row.id === selectedRowId)) return;
setSelectedRowId(rows[0]!.id);
}, [rows, selectedRowId]);
// A null snapshot is still in flight; only a loaded one can say "no coverage".
if (!financials || (!hasAnnualStatements && !hasQuarterlyStatements)) {
return (
);
}
return (
focused={focused}
headerScrollRef={headerScrollRef}
scrollRef={bodyScrollRef}
syncHeaderScroll={syncHeaderScroll}
headerScrollId={headerScrollId}
bodyScrollId={bodyScrollId}
columns={columns}
items={rows}
selection={{
kind: "id",
selectedId: selectedRowId,
getId: (row) => row.id,
onChange: (_id, row, _index, reason) => {
setSelectedRowId(row.id);
if (reason === "pointer" && row.kind === "group" && row.toggleable) {
toggleGroup(row.id);
}
},
}}
sortColumnId={null}
sortDirection="desc"
getItemKey={financialRowKey}
onActivate={(row) => {
if (row.kind === "group" && row.toggleable) toggleGroup(row.id);
}}
getRowBackgroundColor={financialRowBackground}
renderCell={renderCell}
emptyStateTitle="No financial data"
getExportMetadata={() => [
["Currency", comparisonCurrency ?? "per column"],
["Growth", growthBasis],
]}
showHorizontalScrollbar
resetScrollKey={`${resolvedPeriod}:${subTab.key}:${displayStatements.length}`}
rootBefore={(
({ label: tab.name, value: String(index) })),
onChange: (value: string) => setSubTabIdx(Number(value)) },
]}
view={{
value: isAnnual ? "annual" : "quarterly",
options: [
{ label: "Annual", value: "annual", disabled: !hasAnnualStatements },
{ label: "Quarterly", value: "quarterly", disabled: !hasQuarterlyStatements },
],
onChange: (value: string) => setPeriod(value as FinancialPeriod),
}}
meta={[comparisonCurrency, growthBasis].filter(Boolean).join(" ยท ")}
/>
)}
/>
);
}