WIP main worktree changes before merge

This commit is contained in:
2026-03-13 00:20:22 -04:00
parent 58bf80189d
commit e5141238fb
25 changed files with 940 additions and 208 deletions

View File

@@ -210,4 +210,86 @@ describe('statement view model', () => {
row: { key: 'gp_unmapped', residualFlag: true }
});
});
it('renders unmapped detail rows in a dedicated residual section and counts them', () => {
const model = buildStatementTree({
surfaceKind: 'income_statement',
rows: [
createSurfaceRow({ key: 'revenue', label: 'Revenue', category: 'revenue', values: { p1: 100 } })
],
statementDetails: {
unmapped: [
createDetailRow({
key: 'unmapped_other_income',
label: 'Other income residual',
parentSurfaceKey: 'unmapped',
values: { p1: 5 },
residualFlag: true
})
]
},
categories: [],
searchQuery: '',
expandedRowKeys: new Set()
});
expect(model.sections).toHaveLength(2);
expect(model.sections[1]).toMatchObject({
key: 'unmapped_residual',
label: 'Unmapped / Residual'
});
expect(model.sections[1]?.nodes[0]).toMatchObject({
kind: 'detail',
row: { key: 'unmapped_other_income', parentSurfaceKey: 'unmapped' }
});
expect(model.visibleNodeCount).toBe(2);
expect(model.totalNodeCount).toBe(2);
});
it('matches search and resolves selection for unmapped detail rows without a real parent surface', () => {
const rows = [
createSurfaceRow({ key: 'revenue', label: 'Revenue', category: 'revenue', values: { p1: 100 } })
];
const statementDetails = {
unmapped: [
createDetailRow({
key: 'unmapped_fx_gain',
label: 'FX gain residual',
parentSurfaceKey: 'unmapped',
values: { p1: 2 },
residualFlag: true
})
]
};
const model = buildStatementTree({
surfaceKind: 'income_statement',
rows,
statementDetails,
categories: [],
searchQuery: 'fx gain',
expandedRowKeys: new Set()
});
expect(model.sections).toHaveLength(1);
expect(model.sections[0]).toMatchObject({
key: 'unmapped_residual',
label: 'Unmapped / Residual'
});
expect(model.visibleNodeCount).toBe(1);
expect(model.totalNodeCount).toBe(2);
const selection = resolveStatementSelection({
surfaceKind: 'income_statement',
rows,
statementDetails,
selection: { kind: 'detail', key: 'unmapped_fx_gain', parentKey: 'unmapped' }
});
expect(selection).toMatchObject({
kind: 'detail',
row: { key: 'unmapped_fx_gain', parentSurfaceKey: 'unmapped' },
parentSurfaceRow: null
});
});
});

View File

@@ -79,6 +79,10 @@ type Categories = Array<{
count: number;
}>;
const UNMAPPED_DETAIL_GROUP_KEY = 'unmapped';
const UNMAPPED_SECTION_KEY = 'unmapped_residual';
const UNMAPPED_SECTION_LABEL = 'Unmapped / Residual';
function surfaceConfigForKind(surfaceKind: Extract<FinancialSurfaceKind, 'income_statement' | 'balance_sheet' | 'cash_flow_statement'>) {
return SURFACE_CHILDREN[surfaceKind] ?? {};
}
@@ -129,6 +133,25 @@ function sortDetailRows(left: DetailFinancialRow, right: DetailFinancialRow) {
return left.label.localeCompare(right.label);
}
function buildUnmappedDetailNodes(input: {
statementDetails: SurfaceDetailMap | null;
searchQuery: string;
}) {
const normalizedSearch = normalize(input.searchQuery);
return [...(input.statementDetails?.[UNMAPPED_DETAIL_GROUP_KEY] ?? [])]
.sort(sortDetailRows)
.filter((detail) => normalizedSearch.length === 0 || searchTextForDetail(detail).includes(normalizedSearch))
.map((detail) => ({
kind: 'detail',
id: detailNodeId(UNMAPPED_DETAIL_GROUP_KEY, detail),
level: 0,
row: detail,
parentSurfaceKey: UNMAPPED_DETAIL_GROUP_KEY,
matchesSearch: normalizedSearch.length > 0 && searchTextForDetail(detail).includes(normalizedSearch)
}) satisfies StatementTreeDetailNode);
}
function countNodes(nodes: StatementTreeNode[]) {
let count = 0;
@@ -223,10 +246,26 @@ export function buildStatementTree(input: {
.filter((node): node is StatementTreeSurfaceNode => Boolean(node));
if (input.categories.length === 0) {
const sections: StatementTreeSection[] = rootNodes.length > 0
? [{ key: 'ungrouped', label: null, nodes: rootNodes }]
: [];
const unmappedNodes = buildUnmappedDetailNodes({
statementDetails: input.statementDetails,
searchQuery: input.searchQuery
});
if (unmappedNodes.length > 0) {
sections.push({
key: UNMAPPED_SECTION_KEY,
label: UNMAPPED_SECTION_LABEL,
nodes: unmappedNodes
});
}
return {
sections: [{ key: 'ungrouped', label: null, nodes: rootNodes }],
sections,
autoExpandedKeys,
visibleNodeCount: countNodes(rootNodes),
visibleNodeCount: sections.reduce((sum, section) => sum + countNodes(section.nodes), 0),
totalNodeCount: input.rows.length + Object.values(input.statementDetails ?? {}).reduce((sum, rows) => sum + rows.length, 0)
};
}
@@ -254,6 +293,18 @@ export function buildStatementTree(input: {
});
}
const unmappedNodes = buildUnmappedDetailNodes({
statementDetails: input.statementDetails,
searchQuery: input.searchQuery
});
if (unmappedNodes.length > 0) {
sections.push({
key: UNMAPPED_SECTION_KEY,
label: UNMAPPED_SECTION_LABEL,
nodes: unmappedNodes
});
}
return {
sections,
autoExpandedKeys,
@@ -297,9 +348,11 @@ export function resolveStatementSelection(input: {
}
const parentSurfaceKey = selection.parentKey ?? null;
const detailRows = parentSurfaceKey
? input.statementDetails?.[parentSurfaceKey] ?? []
: Object.values(input.statementDetails ?? {}).flat();
const detailRows = parentSurfaceKey === UNMAPPED_DETAIL_GROUP_KEY
? input.statementDetails?.[UNMAPPED_DETAIL_GROUP_KEY] ?? []
: parentSurfaceKey
? input.statementDetails?.[parentSurfaceKey] ?? []
: Object.values(input.statementDetails ?? {}).flat();
const row = detailRows.find((candidate) => candidate.key === selection.key) ?? null;
if (!row) {