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
|
## Known issues
|
||||||
|
|
||||||
Открытые проблемы ветки (ревью, раунд 3), которые исправляет автор PR; спека описывает целевое поведение.
|
Открытая проблема на момент слияния (решение владельца 23.10.2026 — сливать, остаток в техдолг AURORA-1799):
|
||||||
|
|
||||||
1. Поиск в дереве счетов: отметки считаются по отфильтрованному дереву, отметка единственного видимого субсчёта отмечает
|
1. Выбрана одна сторона Дт/Кт: «Экспорт» недоступен, но таблица показывает прежние данные, подсказка — всплывающее уведомление
|
||||||
родителя, и на бэк уходит весь родительский счёт (`TreeFilterDropdown.tsx`).
|
и постоянный текст внутри выпадающего окна, текста у таблицы и у фильтров нет. Целевое поведение — решение (ж).
|
||||||
2. Выбрана одна сторона Дт/Кт: таблица показывает прежние данные, подсказка — всплывающее уведомление и постоянный текст внутри
|
|
||||||
выпадающего окна, «Экспорт» активен и выгружает режим `all`.
|
Пункты раунда 3 про поиск в дереве счетов и стили закрыты ревьюером (решение (п)).
|
||||||
3. Стили: правило тегов в `App.css` и класс на форме ЮЛ (`CatalogueJurForm.tsx`) — вне экрана; у селекта Кт класса нет;
|
|
||||||
`.resRow`, `.empty-cell` не под `.chessTable`; у `Tree` снят `className='filterTree'`.
|
|
||||||
|
|
||||||
## Decisions made without discussion
|
## Decisions made without discussion
|
||||||
|
|
||||||
@ -91,3 +89,9 @@
|
|||||||
- **2026-10-22 (о). Расхождение в скобках всегда**, в том числе нулевое — `(0,00)`: в API `variance_amount` — модуль разности
|
- **2026-10-22 (о). Расхождение в скобках всегда**, в том числе нулевое — `(0,00)`: в API `variance_amount` — модуль разности
|
||||||
(неотрицательный), скобки в макете 31-01 — способ показа третьей строки, а не знак. Значок пропущенных строк ОСВ — оранжевый
|
(неотрицательный), скобки в макете 31-01 — способ показа третьей строки, а не знак. Значок пропущенных строк ОСВ — оранжевый
|
||||||
`WarningOutlined` у первого числа. Подпись «Период» (не «Квартал») — как в макете. Цена ошибки: правка вида ячейки.
|
`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.1 Спека `intercompany-matrix-ui` (ревьюер; с раунда 3 — целевое поведение).
|
||||||
- [x] 1.2 Тесты форматтера сумм `utils/chessTableCellFormatValue.test.ts` (ревьюер).
|
- [x] 1.2 Тесты форматтера сумм `utils/chessTableCellFormatValue.test.ts` (ревьюер).
|
||||||
- [x] 1.3 Желательные пункты ревью раунда 3 (ревьюер, решение (м) в `design.md`).
|
- [x] 1.3 Желательные пункты ревью раунда 3 (ревьюер, решение (м) в `design.md`).
|
||||||
- [ ] 1.4 Автор исправляет «Known issues» из `design.md`.
|
- [x] 1.4 «Known issues» раунда 3: автор (6b956f7, 79d4008) и ревьюер (решение (п)); пункт про одну сторону Дт/Кт — 2.2.
|
||||||
- [ ] 1.5 Перед слиянием: `make web-check`, `npm test`, `npm run lint` — зелёные, вывод в описание PR.
|
- [x] 1.5 Перед слиянием: `make web-check`, `npm test`, `npm run lint` — зелёные, вывод в описание PR.
|
||||||
- [ ] 1.6 Сверка экрана с макетом 31-01 / 31-01-01 в браузере (`docs/mockup/INDEX.md`), в том числе полноэкранный режим.
|
- [x] 1.6 Сверка экрана с макетом 31-01 / 31-01-01 в браузере (`docs/mockup/INDEX.md`), в том числе полноэкранный режим.
|
||||||
|
|
||||||
## 2. После слияния
|
## 2. После слияния
|
||||||
|
|
||||||
- [ ] 2.1 Итоги «Итого» из API вместо клиентских — AURORA-1756 (бэк), AURORA-1757 (фронт).
|
- [ ] 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 { SearchOutlined } from '@ant-design/icons';
|
||||||
import { Button, Input, Space, Tree } from 'antd';
|
import { Button, Input, Space, Tree } from 'antd';
|
||||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { toggleTreeCheck } from '../../utils/treeChecks';
|
||||||
import './TreeFilterDropdown.css';
|
import './TreeFilterDropdown.css';
|
||||||
|
|
||||||
export type OptionType = {
|
export type OptionType = {
|
||||||
@ -98,68 +99,7 @@ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
function checkHandler(keys: string[], info: { node: OptionType; checked: boolean }) {
|
function checkHandler(keys: string[], info: { node: OptionType; checked: boolean }) {
|
||||||
const newKeys = new Set([...keys, ...selectedKeys]);
|
setCheckedKeys(toggleTreeCheck(treeData, checkedKeys, keys, info.node.key, info.checked));
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCheckedKeys(selectedKeys);
|
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='Альтернативные наименования'
|
label='Альтернативные наименования'
|
||||||
extra='Введите синоним и нажмите Enter. Можно добавить несколько значений.'>
|
extra='Введите синоним и нажмите Enter. Можно добавить несколько значений.'>
|
||||||
<Select
|
<Select
|
||||||
className='custom-select-selection-item'
|
|
||||||
mode='tags'
|
mode='tags'
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder='Добавить синоним'
|
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 type { Account, MatrixCompany, Quarter } from '../../types/api';
|
||||||
import { quarterShortLabel } from '../../utils/labels';
|
import { quarterShortLabel } from '../../utils/labels';
|
||||||
import { useNotificationApi } from '../../utils/useNotification';
|
import { useNotificationApi } from '../../utils/useNotification';
|
||||||
|
import './ConsolidationChessTableFilter.css';
|
||||||
|
|
||||||
interface ConsolidationChessTableFilterI {
|
interface ConsolidationChessTableFilterI {
|
||||||
companies: string[];
|
companies: string[];
|
||||||
@ -148,7 +149,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
|
|||||||
variant='underlined'
|
variant='underlined'
|
||||||
showSearch={false}
|
showSearch={false}
|
||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
className='custom-select-selection-item '
|
className='custom-select-selection-item'
|
||||||
popupRender={() => (
|
popupRender={() => (
|
||||||
<TreeFilterDropdown
|
<TreeFilterDropdown
|
||||||
title='Компании'
|
title='Компании'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user