fix(web): шахматка — отметки дерева счетов по полному дереву, стили тегов в css фильтра (AURORA-1538, ревью #158)
- toggleTreeCheck (utils/treeChecks): родитель отмечен, только если отмечены все потомки, на всех уровнях; скрытые поиском отметки берутся из состояния выпадашки — снятые подряд отметки больше не возвращаются; тесты. - Правило тегов из App.css → ConsolidationChessTableFilter.css (App.css пустой, как в main); класс с формы ЮЛ снят. - Спека: Known issues — остаток (одна сторона Дт/Кт) в техдолг AURORA-1799, решение (п), tasks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
79d4008fcd
commit
fd3bb79a9b
@ -35,14 +35,12 @@
|
||||
|
||||
## Known issues
|
||||
|
||||
Открытые проблемы ветки (ревью, раунд 3), которые исправляет автор PR; спека описывает целевое поведение.
|
||||
Открытая проблема на момент слияния (решение владельца 23.10.2026 — сливать, остаток в техдолг AURORA-1799):
|
||||
|
||||
1. Поиск в дереве счетов: отметки считаются по отфильтрованному дереву, отметка единственного видимого субсчёта отмечает
|
||||
родителя, и на бэк уходит весь родительский счёт (`TreeFilterDropdown.tsx`).
|
||||
2. Выбрана одна сторона Дт/Кт: таблица показывает прежние данные, подсказка — всплывающее уведомление и постоянный текст внутри
|
||||
выпадающего окна, «Экспорт» активен и выгружает режим `all`.
|
||||
3. Стили: правило тегов в `App.css` и класс на форме ЮЛ (`CatalogueJurForm.tsx`) — вне экрана; у селекта Кт класса нет;
|
||||
`.resRow`, `.empty-cell` не под `.chessTable`; у `Tree` снят `className='filterTree'`.
|
||||
1. Выбрана одна сторона Дт/Кт: «Экспорт» недоступен, но таблица показывает прежние данные, подсказка — всплывающее уведомление
|
||||
и постоянный текст внутри выпадающего окна, текста у таблицы и у фильтров нет. Целевое поведение — решение (ж).
|
||||
|
||||
Пункты раунда 3 про поиск в дереве счетов и стили закрыты ревьюером (решение (п)).
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
@ -91,3 +89,9 @@
|
||||
- **2026-10-22 (о). Расхождение в скобках всегда**, в том числе нулевое — `(0,00)`: в API `variance_amount` — модуль разности
|
||||
(неотрицательный), скобки в макете 31-01 — способ показа третьей строки, а не знак. Значок пропущенных строк ОСВ — оранжевый
|
||||
`WarningOutlined` у первого числа. Подпись «Период» (не «Квартал») — как в макете. Цена ошибки: правка вида ячейки.
|
||||
- **2026-10-23 (п). Остаток раунда 3 закрыт ревьюером, пункт про одну сторону Дт/Кт — в техдолг** (решение владельца: «доправим
|
||||
сами, потом сольём»). Отметки дерева — чистая функция `utils/treeChecks.toggleTreeCheck` (правило (з) снизу вверх по полному
|
||||
дереву на всех уровнях; скрытые поиском отметки берутся из состояния выпадашки, а не из применённого фильтра — иначе снятые
|
||||
отметки возвращались), тесты `utils/treeChecks.test.ts`. Правило тегов из `App.css` перенесено в
|
||||
`ConsolidationChessTableFilter.css`, класс с формы ЮЛ снят. Пустая таблица с подсказкой при одной стороне — AURORA-1799.
|
||||
Цена ошибки: при одной стороне до AURORA-1799 на экране прежние данные, выгрузка недоступна.
|
||||
|
||||
@ -3,10 +3,11 @@
|
||||
- [x] 1.1 Спека `intercompany-matrix-ui` (ревьюер; с раунда 3 — целевое поведение).
|
||||
- [x] 1.2 Тесты форматтера сумм `utils/chessTableCellFormatValue.test.ts` (ревьюер).
|
||||
- [x] 1.3 Желательные пункты ревью раунда 3 (ревьюер, решение (м) в `design.md`).
|
||||
- [ ] 1.4 Автор исправляет «Known issues» из `design.md`.
|
||||
- [ ] 1.5 Перед слиянием: `make web-check`, `npm test`, `npm run lint` — зелёные, вывод в описание PR.
|
||||
- [ ] 1.6 Сверка экрана с макетом 31-01 / 31-01-01 в браузере (`docs/mockup/INDEX.md`), в том числе полноэкранный режим.
|
||||
- [x] 1.4 «Known issues» раунда 3: автор (6b956f7, 79d4008) и ревьюер (решение (п)); пункт про одну сторону Дт/Кт — 2.2.
|
||||
- [x] 1.5 Перед слиянием: `make web-check`, `npm test`, `npm run lint` — зелёные, вывод в описание PR.
|
||||
- [x] 1.6 Сверка экрана с макетом 31-01 / 31-01-01 в браузере (`docs/mockup/INDEX.md`), в том числе полноэкранный режим.
|
||||
|
||||
## 2. После слияния
|
||||
|
||||
- [ ] 2.1 Итоги «Итого» из API вместо клиентских — AURORA-1756 (бэк), AURORA-1757 (фронт).
|
||||
- [ ] 2.2 Одна сторона Дт/Кт — пустая таблица с подсказкой, без уведомления — AURORA-1799.
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
.custom-select-selection-item .ant-select-selection-item {
|
||||
background-color: #ecfdf3 !important;
|
||||
color: #1f5f35 !important;
|
||||
box-shadow: inset 0 0 0 1px #2ea64b38 !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Space, Tree } from 'antd';
|
||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { toggleTreeCheck } from '../../utils/treeChecks';
|
||||
import './TreeFilterDropdown.css';
|
||||
|
||||
export type OptionType = {
|
||||
@ -98,68 +99,7 @@ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
|
||||
};
|
||||
|
||||
function checkHandler(keys: string[], info: { node: OptionType; checked: boolean }) {
|
||||
const newKeys = new Set([...keys, ...selectedKeys]);
|
||||
|
||||
const getAllDescendants = (node: OptionType): string[] => {
|
||||
let res: string[] = [];
|
||||
if (node?.children) {
|
||||
node.children.forEach((child) => {
|
||||
res.push(child.key);
|
||||
res = [...res, ...getAllDescendants(child)];
|
||||
});
|
||||
}
|
||||
return res;
|
||||
};
|
||||
const findNodeByKey = (nodes: OptionType[], key: string): OptionType | undefined => {
|
||||
for (const node of nodes) {
|
||||
if (node.key === key) return node;
|
||||
if (node.children) {
|
||||
const found = findNodeByKey(node.children, key);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
function findParentByKey(nodes: OptionType[], targetKey: string, parent: OptionType | null = null): OptionType | undefined {
|
||||
for (const node of nodes) {
|
||||
if (node.key === targetKey) {
|
||||
return parent || undefined;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
const foundParent = findParentByKey(node.children, targetKey, node);
|
||||
if (foundParent) {
|
||||
return foundParent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const node = findNodeByKey(treeData, info.node.key);
|
||||
const childrens = getAllDescendants(node as OptionType);
|
||||
if (node?.key) {
|
||||
if (info.checked) {
|
||||
newKeys.add(node.key);
|
||||
childrens?.forEach((elem) => newKeys.add(elem));
|
||||
const parentNode = findParentByKey(treeData, node.key);
|
||||
if (parentNode?.key) {
|
||||
const childrens = getAllDescendants(parentNode as OptionType);
|
||||
const isAllChildrenSelected = childrens.every((childKey) => newKeys.has(childKey));
|
||||
if (!isAllChildrenSelected) {
|
||||
newKeys.delete(parentNode.key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const parentNode = findParentByKey(treeData, node.key);
|
||||
if (parentNode?.key) {
|
||||
newKeys.delete(parentNode?.key);
|
||||
}
|
||||
newKeys.delete(node.key);
|
||||
childrens?.forEach((elem) => newKeys.delete(elem));
|
||||
}
|
||||
}
|
||||
|
||||
setCheckedKeys(Array.from(newKeys));
|
||||
setCheckedKeys(toggleTreeCheck(treeData, checkedKeys, keys, info.node.key, info.checked));
|
||||
}
|
||||
useEffect(() => {
|
||||
setCheckedKeys(selectedKeys);
|
||||
|
||||
45
web/src/utils/treeChecks.test.ts
Normal file
45
web/src/utils/treeChecks.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { type TreeCheckNode, toggleTreeCheck } from './treeChecks';
|
||||
|
||||
// 60 → 60.01, 60.02; 60.02 → 60.02.1, 60.02.2; 62 без детей
|
||||
const tree: TreeCheckNode[] = [
|
||||
{
|
||||
key: '60',
|
||||
children: [{ key: '60.01' }, { key: '60.02', children: [{ key: '60.02.1' }, { key: '60.02.2' }] }],
|
||||
},
|
||||
{ key: '62' },
|
||||
];
|
||||
|
||||
const sorted = (keys: string[]) => [...keys].sort();
|
||||
|
||||
describe('toggleTreeCheck', () => {
|
||||
it('отметка родителя отмечает всех потомков', () => {
|
||||
expect(sorted(toggleTreeCheck(tree, [], [], '60', true))).toEqual(['60', '60.01', '60.02', '60.02.1', '60.02.2']);
|
||||
});
|
||||
|
||||
it('единственный найденный субсчёт не отмечает родителя, даже если дерево поиска прислало родителя', () => {
|
||||
// при поиске у 60 виден только 60.01, и Tree сам дописывает 60 в отмеченные
|
||||
expect(sorted(toggleTreeCheck(tree, [], ['60', '60.01'], '60.01', true))).toEqual(['60.01']);
|
||||
});
|
||||
|
||||
it('родитель отмечается, когда отмечены все его субсчета, на всех уровнях', () => {
|
||||
const keys = toggleTreeCheck(tree, ['60.01', '60.02.1'], [], '60.02.2', true);
|
||||
expect(sorted(keys)).toEqual(['60', '60.01', '60.02', '60.02.1', '60.02.2']);
|
||||
});
|
||||
|
||||
it('снятие субсчёта снимает родителя и предков, остальные отметки остаются', () => {
|
||||
const all = ['60', '60.01', '60.02', '60.02.1', '60.02.2', '62'];
|
||||
expect(sorted(toggleTreeCheck(tree, all, [], '60.02.1', false))).toEqual(['60.01', '60.02.2', '62']);
|
||||
});
|
||||
|
||||
it('снятые подряд отметки не возвращаются', () => {
|
||||
const first = toggleTreeCheck(tree, ['60.01', '62'], ['62'], '60.01', false);
|
||||
expect(first).toEqual(['62']);
|
||||
expect(toggleTreeCheck(tree, first, [], '62', false)).toEqual([]);
|
||||
});
|
||||
|
||||
it('отметки, скрытые поиском, сохраняются', () => {
|
||||
// виден только 62, а 60.01 отмечен раньше и скрыт фильтром
|
||||
expect(sorted(toggleTreeCheck(tree, ['60.01'], ['62'], '62', true))).toEqual(['60.01', '62']);
|
||||
});
|
||||
});
|
||||
44
web/src/utils/treeChecks.ts
Normal file
44
web/src/utils/treeChecks.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export type TreeCheckNode = {
|
||||
key: string;
|
||||
children?: TreeCheckNode[];
|
||||
};
|
||||
|
||||
const descendantKeys = (node: TreeCheckNode): string[] => (node.children ?? []).flatMap((child) => [child.key, ...descendantKeys(child)]);
|
||||
|
||||
const findNode = (nodes: TreeCheckNode[], key: string): TreeCheckNode | undefined => {
|
||||
for (const node of nodes) {
|
||||
if (node.key === key) return node;
|
||||
const found = node.children && findNode(node.children, key);
|
||||
if (found) return found;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Снизу вверх: родитель отмечен, только если отмечены все его дети (а значит, и все потомки)
|
||||
const syncParents = (nodes: TreeCheckNode[], keys: Set<string>) => {
|
||||
nodes.forEach((node) => {
|
||||
if (!node.children?.length) return;
|
||||
syncParents(node.children, keys);
|
||||
if (node.children.every((child) => keys.has(child.key))) keys.add(node.key);
|
||||
else keys.delete(node.key);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Новые отметки дерева после клика по узлу `key`.
|
||||
* Отметки считаются по полному дереву, а не по найденному поиском: `treeKeys` (что прислал Tree по видимым узлам)
|
||||
* может содержать родителя, у которого виден один субсчёт, — он снимается, если отмечены не все субсчета.
|
||||
* Отметки, скрытые поиском, берутся из `prevKeys` (текущее состояние выпадашки, не применённый фильтр).
|
||||
*/
|
||||
export const toggleTreeCheck = (tree: TreeCheckNode[], prevKeys: string[], treeKeys: string[], key: string, checked: boolean): string[] => {
|
||||
const keys = new Set([...prevKeys, ...treeKeys]);
|
||||
const node = findNode(tree, key);
|
||||
if (node) {
|
||||
for (const k of [node.key, ...descendantKeys(node)]) {
|
||||
if (checked) keys.add(k);
|
||||
else keys.delete(k);
|
||||
}
|
||||
}
|
||||
syncParents(tree, keys);
|
||||
return Array.from(keys);
|
||||
};
|
||||
@ -213,7 +213,6 @@ export const CatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, jurId
|
||||
label='Альтернативные наименования'
|
||||
extra='Введите синоним и нажмите Enter. Можно добавить несколько значений.'>
|
||||
<Select
|
||||
className='custom-select-selection-item'
|
||||
mode='tags'
|
||||
style={{ width: '100%' }}
|
||||
placeholder='Добавить синоним'
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
/* Теги выбранных значений в селектах фильтра шахматки */
|
||||
.custom-select-selection-item .ant-select-selection-item {
|
||||
background-color: #ecfdf3 !important;
|
||||
color: #1f5f35 !important;
|
||||
box-shadow: inset 0 0 0 1px #2ea64b38 !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
@ -6,6 +6,7 @@ import { type OptionType, TreeFilterDropdown } from '../../components/TreeFilter
|
||||
import type { Account, MatrixCompany, Quarter } from '../../types/api';
|
||||
import { quarterShortLabel } from '../../utils/labels';
|
||||
import { useNotificationApi } from '../../utils/useNotification';
|
||||
import './ConsolidationChessTableFilter.css';
|
||||
|
||||
interface ConsolidationChessTableFilterI {
|
||||
companies: string[];
|
||||
@ -148,7 +149,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
|
||||
variant='underlined'
|
||||
showSearch={false}
|
||||
popupMatchSelectWidth={false}
|
||||
className='custom-select-selection-item '
|
||||
className='custom-select-selection-item'
|
||||
popupRender={() => (
|
||||
<TreeFilterDropdown
|
||||
title='Компании'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user