Files
iistwin/server/finance/category-matching.ts

92 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Матчинг строк финансовых таблиц по правилам категорий (деревья).
// Переиспользуется 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;
}