WIP main worktree changes before merge
This commit is contained in:
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user