Финансы: деревья категорий как в Data-Insight2 — группировка авто/ручные, свёрнуто по умолчанию, полосы сумм, нераспределённые
This commit is contained in:
@@ -34,3 +34,58 @@ export function applyCategoryRules(row: Record<string, any>, categories: any[]):
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { tenantIsolation } from "../middleware/tenant.middleware";
|
||||
import { previewCustomerSync, applyCustomerSync, type SyncApplyCustomer } from "./customer-sync";
|
||||
import { previewContractSync, applyContractSync, type SyncApplyContract } from "./contract-sync";
|
||||
import { getFinanceSettings, setFinanceSettings } from "./settings";
|
||||
import { applyCategoryRules } from "./category-matching";
|
||||
import { applyCategoryRules, buildGroupedTrees } from "./category-matching";
|
||||
import { registerDebtRoutes } from "./debt";
|
||||
import { registerCalculationRoutes } from "./calculations";
|
||||
import { registerAuditRoutes, startAuditScheduler } from "./audit";
|
||||
@@ -239,14 +239,12 @@ function registerIncomeRoutes(app: Express) {
|
||||
}).filter((n) => n.total > 0 || (n.children && n.children.length > 0));
|
||||
}
|
||||
|
||||
const trees = categories.map((tree) => ({
|
||||
id: tree.id,
|
||||
name: tree.name,
|
||||
isPrimary: tree.treeId === "primary",
|
||||
total: (treeTotals.get(tree.id) || { total: 0 }).total,
|
||||
count: (treeTotals.get(tree.id) || { count: 0 }).count,
|
||||
nodes: buildTreeNodes(tree.children || []),
|
||||
}));
|
||||
const trees = buildGroupedTrees(
|
||||
categories,
|
||||
getIncomeAutoDetectConfig(),
|
||||
treeTotals,
|
||||
(roots) => buildTreeNodes(roots)
|
||||
);
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
@@ -436,12 +434,12 @@ function registerExpenseRoutes(app: Express) {
|
||||
}).filter((n) => n.total > 0 || (n.children && n.children.length > 0));
|
||||
}
|
||||
|
||||
const trees = categories.map((tree) => ({
|
||||
id: tree.id, name: tree.name, isPrimary: tree.treeId === "primary",
|
||||
total: (treeTotals.get(tree.id) || { total: 0 }).total,
|
||||
count: (treeTotals.get(tree.id) || { count: 0 }).count,
|
||||
nodes: buildTreeNodes(tree.children || []),
|
||||
}));
|
||||
const trees = buildGroupedTrees(
|
||||
categories,
|
||||
getExpenseAutoDetectConfig(),
|
||||
treeTotals,
|
||||
(roots) => buildTreeNodes(roots)
|
||||
);
|
||||
|
||||
const response = { success: true, trees, uncategorized, grandTotal: { total: grandTotal, count: rows.length } };
|
||||
cache.set(cacheKey, response);
|
||||
|
||||
Reference in New Issue
Block a user