import type { DataTable, DataTableRow } from "@shared/schema"; // Узел дерева строк справочника: // group — виртуальная группа (по значению колонки или строка с детьми), // row — конкретная строка справочника. export type DataTableTreeNode = | { type: 'group'; id: string; label: string; level: number; children: DataTableTreeNode[] } | { type: 'row'; id: number; label: string; level: number; row: DataTableRow }; export interface DataTableTreeResult { columnIndex: number | null; treeColumnIndex: number | null; treeColumns: number[]; treeDisplayColumns: number[]; displayColumnIndex: number; nodes: DataTableTreeNode[]; } export interface BuildDataTableTreeOptions { // Фильтры: индекс колонки (строкой) → значение; сравнение точное, без учёта регистра. filters?: Record; // Подстрока для фильтрации узлов дерева по label (без учёта регистра). search?: string; } // Строит дерево строк справочника. // Два режима (как в GET /api/tables/:tableId/rows/tree): // - treeColumns заданы → группировка по значениям этих колонок (мультиуровневая); // - иначе → иерархия по parentId. // Логика вынесена из server/routes/data-tables.routes.ts — поведение не менять. export function buildDataTableTree( table: DataTable, rows: DataTableRow[], options?: BuildDataTableTreeOptions ): DataTableTreeResult { let filteredRows = rows; const filters = options?.filters; if (filters) { Object.entries(filters).forEach(([col, val]) => { const ci = parseInt(col); if (!isNaN(ci) && val) { filteredRows = filteredRows.filter((r) => { const v = Array.isArray(r.values) ? r.values : []; return String(v[ci] || '').toLowerCase() === String(val).toLowerCase(); }); } }); } const search = (options?.search ?? '').toLowerCase(); const treeColumnIndex = table.treeColumnIndex; const treeDisplayColumnIndex = table.treeDisplayColumnIndex; const treeColumns = table.treeColumns ?? (treeColumnIndex != null ? [treeColumnIndex] : []); const treeDisplayColumns = table.treeDisplayColumns ?? (treeDisplayColumnIndex != null ? [treeDisplayColumnIndex] : [0]); const displayColumnIndex = treeDisplayColumnIndex ?? treeDisplayColumns[0] ?? 0; const getRowLabel = (row: DataTableRow): string => { const values = Array.isArray(row.values) ? row.values : []; // Последнее заполненное значение из treeDisplayColumns for (let i = treeDisplayColumns.length - 1; i >= 0; i--) { const val = String(values[treeDisplayColumns[i]] ?? '').trim(); if (val) return val; } // Fallback: первая заполненная колонка for (const val of values) { const str = String(val ?? '').trim(); if (str) return str; } return '(без названия)'; }; const buildParentTree = (parentRows: DataTableRow[], parentId: number | null, level = 0): DataTableTreeNode[] => { const items = parentRows .filter((r) => (r.parentId ?? null) === parentId) .sort((a, b) => a.position - b.position); return items.map((row) => { const children = buildParentTree(parentRows, row.id, level + 1); const label = getRowLabel(row); if (children.length > 0) { return { type: 'group' as const, id: `row-group:${row.id}`, label, level, children: [...children, { type: 'row' as const, id: row.id, label, level: level + 1, row }] }; } return { type: 'row' as const, id: row.id, label, level, row }; }); }; const buildMultiColumnTree = (allRows: DataTableRow[], columns: number[], level = 0): DataTableTreeNode[] => { if (level >= columns.length) { return allRows .sort((a, b) => a.position - b.position) .map((row) => ({ type: 'row' as const, id: row.id, label: getRowLabel(row), level, row })); } const columnIndex = columns[level]; const groups = new Map(); const rootRows: DataTableRow[] = []; for (const row of allRows) { const vals = Array.isArray(row.values) ? row.values : []; const key = String(vals[columnIndex] ?? '').trim(); if (!key) { rootRows.push(row); continue; } if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(row); } const nodes: DataTableTreeNode[] = []; for (const [label, groupRows] of groups) { // Если у группы все строки имеют пустое значение в treeDisplayColumns, // группа становится конечной строкой (можно выбрать в задачах) const allEmpty = groupRows.every((row) => { const vals = Array.isArray(row.values) ? row.values : []; return treeDisplayColumns.every((ci) => !String(vals[ci] ?? '').trim()); }); if (allEmpty && groupRows.length === 1) { nodes.push({ type: 'row', id: groupRows[0].id, label, level, row: groupRows[0], }); } else { nodes.push({ type: 'group', id: `group:${level}:${label}`, label, level, children: buildMultiColumnTree(groupRows, columns, level + 1), }); } } if (rootRows.length > 0) { nodes.push(...buildMultiColumnTree(rootRows, columns, level + 1)); } return nodes.sort((a, b) => { const al = a.type === 'group' ? a.label : getRowLabel(a.row); const bl = b.type === 'group' ? b.label : getRowLabel(b.row); return al.localeCompare(bl, 'ru'); }); }; const filterNodes = (nodes: DataTableTreeNode[], query: string): DataTableTreeNode[] => { if (!query) return nodes; const result: DataTableTreeNode[] = []; for (const node of nodes) { if (node.type === 'row') { const label = node.label.toLowerCase(); if (label.includes(query)) result.push(node); } else { const children = filterNodes(node.children, query); if (children.length > 0) { result.push({ ...node, children }); } else if (node.label.toLowerCase().includes(query)) { result.push({ ...node, children: [] }); } } } return result; }; const rawNodes = treeColumns.length > 0 ? buildMultiColumnTree(filteredRows, treeColumns) : buildParentTree(filteredRows, null); const nodes = search ? filterNodes(rawNodes, search) : rawNodes; return { columnIndex: treeColumnIndex, treeColumnIndex, treeColumns, treeDisplayColumns, displayColumnIndex, nodes, }; }