// Матчинг строк финансовых таблиц по правилам категорий (деревья). // Переиспользуется routes.ts (tree-summary, категории) и calculations.ts (налог на прибыль). export function matchRule(row: Record, rule: { column: string; operator: string; value: string }): boolean { const val = row[rule.column]; const strVal = val == null ? "" : String(val); const numVal = parseFloat(strVal); const ruleNum = parseFloat(rule.value); switch (rule.operator) { case "equals": return strVal === rule.value; case "not_equals": return strVal !== rule.value; case "contains": return strVal.toLowerCase().includes(rule.value.toLowerCase()); case "greater_than": return !isNaN(numVal) && !isNaN(ruleNum) && numVal > ruleNum; case "less_than": return !isNaN(numVal) && !isNaN(ruleNum) && numVal < ruleNum; case "equal_number": return !isNaN(numVal) && !isNaN(ruleNum) && numVal === ruleNum; default: return false; } } // Возвращает id глубочайшей совпавшей категории или null export function applyCategoryRules(row: Record, categories: any[]): string | null { for (const cat of categories) { const match = cat.ruleLogic === "or" ? cat.rules.some((r: any) => matchRule(row, r)) : cat.rules.every((r: any) => matchRule(row, r)); if (match) { if (cat.children && cat.children.length > 0) { const child = applyCategoryRules(row, cat.children); if (child) return child; } return cat.id; } } return null; } // Сумма статистики категории со всеми потомками (статистика хранится по id глубочайшей совпавшей категории) function subtreeStats( cat: any, treeTotals: Map ): { total: number; count: number } { const own = treeTotals.get(cat.id) || { total: 0, count: 0 }; let total = own.total; let count = own.count; for (const child of cat.children || []) { const s = subtreeStats(child, treeTotals); total += s.total; count += s.count; } return { total, count }; } // Группировка корневых категорий по деревьям автоопределения (как в Data-Insight2): // корни с treeId из конфигурации дерева — в это дерево, остальные — в «Ручные категории». export function buildGroupedTrees( categories: any[], autoConfig: { trees: Array<{ id: string; name: string; isPrimary?: boolean }> }, treeTotals: Map, buildTreeNodes: (roots: any[]) => any[] ): Array<{ id: string; name: string; isPrimary: boolean; total: number; count: number; nodes: any[] }> { const configIds = new Set(autoConfig.trees.map((t) => t.id)); const manualRoots = categories.filter((c) => !c.treeId || !configIds.has(c.treeId)); const trees: Array<{ id: string; name: string; isPrimary: boolean; total: number; count: number; nodes: any[] }> = []; if (manualRoots.length > 0) { trees.push({ id: "__manual__", name: "Ручные категории", isPrimary: true, total: manualRoots.reduce((s, c) => s + subtreeStats(c, treeTotals).total, 0), count: manualRoots.reduce((s, c) => s + subtreeStats(c, treeTotals).count, 0), nodes: buildTreeNodes(manualRoots), }); } for (const treeCfg of autoConfig.trees) { const roots = categories.filter((c) => c.treeId === treeCfg.id); trees.push({ id: treeCfg.id, name: treeCfg.name, isPrimary: !!treeCfg.isPrimary, total: roots.reduce((s, c) => s + subtreeStats(c, treeTotals).total, 0), count: roots.reduce((s, c) => s + subtreeStats(c, treeTotals).count, 0), nodes: buildTreeNodes(roots), }); } return trees; }