92 lines
3.9 KiB
TypeScript
92 lines
3.9 KiB
TypeScript
// Матчинг строк финансовых таблиц по правилам категорий (деревья).
|
||
// Переиспользуется routes.ts (tree-summary, категории) и calculations.ts (налог на прибыль).
|
||
|
||
export function matchRule(row: Record<string, any>, 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<string, any>, 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<string, { total: number; count: number }>
|
||
): { 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<string, { total: number; count: number }>,
|
||
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;
|
||
}
|
||
|