From edb3a5303ee47dade90cd89dd325b3d3ff4fdc35 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 10 Nov 2026 12:29:37 +0400 Subject: [PATCH] =?UTF-8?q?fix(#235):=20=D1=84=D0=BB=D0=B0=D0=B6=D0=BA?= =?UTF-8?q?=D0=B8=20=C2=AB=D0=A3=D1=87=D0=B8=D1=82=D1=8B=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=20=D0=9A=D0=A4=D0=9E=C2=BB=20=D0=B2=D0=B8=D0=B4?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B1=D0=B5=D0=B7=20=D0=BD=D0=B0=D0=B2=D0=B5?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20(AURORA-2033)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Замечание заказчика 09.11.2026, п. 5: флажки почти не видны, заметны только при наведении. Причины: у агрегата — неактивный (бледно-серый) флажок; у нераспределённой строки рамка флажка (#d9d9d9) сливалась с приглушённым фоном (#f2f4f7), а исключённая строка затемнялась целиком вместе с флажком. - IncludeFlag: флажок с подписью «Да»/«Нет» (как в макете 23.10), у агрегата — зелёная отметка вместо неактивного флажка, рамка флажка контрастнее; - includeView (чистая функция, тесты): что показать в колонке для агрегата, нераспределённой строки и строки ОСВ; - ячейка флажка не приглушается у исключённой строки; - спека corrections-table-ui: требование к видимости статуса. Co-Authored-By: Claude Opus 5.5 --- openspec/specs/corrections-table-ui/spec.md | 10 ++++ .../CorrectionTable/CorrectionTable.tsx | 41 +++++++------- .../widgets/CorrectionTable/IncludeFlag.tsx | 54 +++++++++++++++++++ .../widgets/CorrectionTable/readOnly.test.ts | 39 +++++++++++++- web/src/widgets/CorrectionTable/readOnly.ts | 14 +++++ 5 files changed, 139 insertions(+), 19 deletions(-) create mode 100644 web/src/widgets/CorrectionTable/IncludeFlag.tsx diff --git a/openspec/specs/corrections-table-ui/spec.md b/openspec/specs/corrections-table-ui/spec.md index f716167..fdc5bd3 100644 --- a/openspec/specs/corrections-table-ui/spec.md +++ b/openspec/specs/corrections-table-ui/spec.md @@ -74,6 +74,16 @@ TBD - created by archiving change corrections-table-ui. Update Purpose after arc (поставлен) и затем перечитывать таблицу целиком. Флажок агрегата SHALL быть отмечен и неактивен с подсказкой «Агрегат всегда учитывается в КФО; лишнюю строку уберите из правила»; у строк агрегата флажка SHALL NOT быть (ответ Лысенко 29.10, п. 4). +Статус учёта SHALL читаться без наведения (AURORA-2033, замечание заказчика 09.11.2026; макет 23.10): рядом с флажком — +подпись «Да» (зелёная) или «Нет» (серая); у агрегата вместо бледного неактивного флажка — зелёная отметка «Да»; рамка флажка +нераспределённой строки SHALL быть контрастной к её приглушённому фону. Приглушение исключённой строки SHALL NOT касаться +ячейки флажка — «Нет» остаётся читаемым, строку можно вернуть. + +#### Scenario: Статус учёта виден без наведения +- **WHEN** таблица показывает агрегат и нераспределённую строку с `excluded = true` +- **THEN** у агрегата — зелёная отметка «Да», у строки — снятый флажок с подписью «Нет» в полную яркость, хотя остальные ячейки + строки приглушены + #### Scenario: Исключить нераспределённую строку из КФО - **WHEN** аналитик снимает флажок у нераспределённой строки «ЭПТ» по 76.09 - **THEN** уходит `{action: "EXCLUDE", entry_ids: [...], side: "D"}`, таблица обновляется ответом, строка приглушена diff --git a/web/src/widgets/CorrectionTable/CorrectionTable.tsx b/web/src/widgets/CorrectionTable/CorrectionTable.tsx index d9eb9c9..151bcc9 100644 --- a/web/src/widgets/CorrectionTable/CorrectionTable.tsx +++ b/web/src/widgets/CorrectionTable/CorrectionTable.tsx @@ -1,15 +1,16 @@ // Таблица корректировок: строки агрегатов, их строки ОСВ при раскрытии, нераспределённые, «Итого»; действия над строками — через `actions`. // Фильтры живут в родителе (CorrectionContent); здесь только рисуем и сообщаем о действиях через `actions`. import { DownOutlined, EditOutlined, MoreOutlined, RightOutlined, WarningOutlined } from '@ant-design/icons'; -import { Button, Checkbox, Dropdown, Table, Tag, Tooltip } from 'antd'; +import { Button, Dropdown, Table, Tag, Tooltip } from 'antd'; import type { MenuProps } from 'antd'; import type { ColumnGroupType, ColumnType } from 'antd/es/table'; import { type ReactNode, useMemo } from 'react'; import type { AllocationIn, CorrectionAggregateOut, CorrectionLineOut, CorrectionRuleOut } from '../../types/corrections'; import { HeaderFilter, SelectFilter, TextFilter } from './HeaderFilter'; +import { IncludeFlag } from './IncludeFlag'; import { COLUMN_DEFS, type ColKey } from './columns'; import { percentText } from './editorView'; -import { AGGREGATE_FLAG_HINT, allocationMenu, includeBody, menuBody } from './readOnly'; +import { allocationMenu, includeBody, includeView, menuBody } from './readOnly'; import { type DeviationFilter, type TableFilters, type TableRow, articleCodes, ruleLabel } from './tableModel'; import type { visibleTotals } from './tableModel'; @@ -111,19 +112,20 @@ export function CorrectionTable(props: Props) { const redIf = (text: string): ReactNode => a.exceeds_threshold && text !== '—' ? {text} : text; switch (key) { - case 'include': - // агрегат учитывается в КФО всегда: флажок отмечен и закреплён, лишняя строка убирается из правила - return withHint( - - + case 'include': { + // агрегат учитывается в КФО всегда: галочка закреплена, лишняя строка убирается из правила + const view = includeView(row, readOnlyHint); + return ( + + {view && } {a.composition_changed && ( )} - , - AGGREGATE_FLAG_HINT, + ); + } case 'd_entity': return a.company_1.name; case 'k_entity': @@ -181,17 +183,19 @@ export function CorrectionTable(props: Props) { const owned = sideSuffix(row); // 'D' | 'K' const own = (side: 'D' | 'K') => owned === side; switch (key) { - case 'include': + case 'include': { // флажок — только у нераспределённой строки; строка агрегата учитывается вместе с агрегатом - if (row.kind !== 'unallocated') return null; - return withHint( - actions.onAllocate(includeBody(e.entry_ids, e.side, ev.target.checked))} - />, + ariaLabel={`Учитывать в КФО: ${e.account_code}`} + onChange={(checked) => actions.onAllocate(includeBody(e.entry_ids, e.side, checked))} + /> ); + } case 'd_entity': if (row.kind !== 'unallocated') return null; return own('D') ? row.entry.owner.name : row.entry.counterparty.name; @@ -244,7 +248,8 @@ export function CorrectionTable(props: Props) { onCell: (row) => ({ style: { background: rowBg(row) ?? tint(key), - opacity: row.kind === 'unallocated' && row.entry.excluded ? 0.5 : 1, + // исключённая строка приглушена, но не ячейка флажка: «Нет» должно читаться, чтобы строку можно было вернуть + opacity: key !== 'include' && row.kind === 'unallocated' && row.entry.excluded ? 0.5 : 1, }, }), }); diff --git a/web/src/widgets/CorrectionTable/IncludeFlag.tsx b/web/src/widgets/CorrectionTable/IncludeFlag.tsx new file mode 100644 index 0000000..46ab087 --- /dev/null +++ b/web/src/widgets/CorrectionTable/IncludeFlag.tsx @@ -0,0 +1,54 @@ +// Флажок «Учитывать в КФО» с подписью «Да»/«Нет» (AURORA-2033, макет 23.10). Статус читается по подписи и цвету даже +// там, где флажок неактивен; у агрегата вместо бледного неактивного флажка — зелёная галочка (учитывается всегда). +import { CheckSquareFilled } from '@ant-design/icons'; +import { Checkbox, ConfigProvider, Tooltip } from 'antd'; +import type { ThemeConfig } from 'antd'; +import type { IncludeView } from './readOnly'; + +const COLOR_ON = '#2a8c3a'; +const COLOR_OFF = '#667085'; + +// Рамка флажка по умолчанию (#d9d9d9) сливается с серой заливкой нераспределённой строки — делаем её темнее; +// неактивный флажок (правки закрыты) — тоже контрастнее стандартного. Объект вне компонента: один и тот же для всех ячеек. +const FLAG_THEME: ThemeConfig = { + components: { Checkbox: { colorBorder: '#98a2b3', colorBgContainerDisabled: '#eaecf0', colorTextDisabled: COLOR_OFF } }, +}; + +type Props = { + view: IncludeView; + /** Флажок неактивен: правки закрыты или идёт запрос. У закреплённого агрегата не используется. */ + disabled?: boolean; + /** Подпись для экранного диктора: что за строка. */ + ariaLabel: string; + onChange?: (checked: boolean) => void; +}; + +export function IncludeFlag({ view, disabled = false, ariaLabel, onChange }: Props) { + // высота строки = высоте флажка (16px): иначе подпись стоит выше его центра + const text = ( + {view.label} + ); + const node = view.locked ? ( + + + {text} + + ) : ( + + + onChange?.(e.target.checked)} /> + + {text} + + ); + return view.hint ? ( + + {node} + + ) : ( + node + ); +} diff --git a/web/src/widgets/CorrectionTable/readOnly.test.ts b/web/src/widgets/CorrectionTable/readOnly.test.ts index 7720c45..b42fcd0 100644 --- a/web/src/widgets/CorrectionTable/readOnly.test.ts +++ b/web/src/widgets/CorrectionTable/readOnly.test.ts @@ -1,7 +1,17 @@ import { describe, expect, it } from 'vitest'; import { ApiError } from '../../api/client'; import { makeMockTable } from './mockData'; -import { COMMENT_MAX_LENGTH, allocationMenu, errorOutcome, includeBody, menuBody, needsReload, readOnlyHint } from './readOnly'; +import { + AGGREGATE_FLAG_HINT, + COMMENT_MAX_LENGTH, + allocationMenu, + errorOutcome, + includeBody, + includeView, + menuBody, + needsReload, + readOnlyHint, +} from './readOnly'; import type { TableRow } from './tableModel'; describe('readOnlyHint', () => { @@ -149,3 +159,30 @@ describe('errorOutcome', () => { it('лимит комментария — 1000 (контракт §3.2)', () => { expect(COMMENT_MAX_LENGTH).toBe(1000); }); + +describe('includeView', () => { + const t = makeMockTable(); + const aggregateRow: TableRow = { kind: 'aggregate', key: 'a', aggregate: t.aggregates[0] }; + const unallocRow = (excluded: boolean): TableRow => ({ kind: 'unallocated', key: 'u', entry: { ...t.unallocated[0], excluded } }); + + it('агрегат: «Да», закреплён, подсказка — всегда учитывается', () => { + expect(includeView(aggregateRow, null)).toEqual({ checked: true, locked: true, label: 'Да', hint: AGGREGATE_FLAG_HINT }); + }); + it('агрегат: подсказка про агрегат и при закрытых правках', () => { + expect(includeView(aggregateRow, 'Возьмите консолидацию в работу')?.hint).toBe(AGGREGATE_FLAG_HINT); + }); + it('нераспределённая учитывается: «Да», переключается', () => { + expect(includeView(unallocRow(false), null)).toEqual({ checked: true, locked: false, label: 'Да', hint: null }); + }); + it('нераспределённая исключена: «Нет»', () => { + expect(includeView(unallocRow(true), null)).toMatchObject({ checked: false, label: 'Нет' }); + }); + it('правки закрыты — подсказка «почему»', () => { + expect(includeView(unallocRow(false), 'Возьмите консолидацию в работу')?.hint).toBe('Возьмите консолидацию в работу'); + }); + it('строка ОСВ внутри агрегата флажка не имеет', () => { + const a = t.aggregates[0]; + const entry = [...a.side_1.entries, ...a.side_2.entries][0]; + expect(includeView({ kind: 'entry', key: 'e', parentKey: 'a', side: 1, entry }, null)).toBeNull(); + }); +}); diff --git a/web/src/widgets/CorrectionTable/readOnly.ts b/web/src/widgets/CorrectionTable/readOnly.ts index f63e514..eecddcf 100644 --- a/web/src/widgets/CorrectionTable/readOnly.ts +++ b/web/src/widgets/CorrectionTable/readOnly.ts @@ -40,6 +40,20 @@ export const COMMENT_MAX_LENGTH = 1000; /** Подсказка у флажка агрегата: строки агрегата учитываются всегда, лишнюю строку из него убирают. */ export const AGGREGATE_FLAG_HINT = 'Агрегат всегда учитывается в КФО; лишнюю строку уберите из правила'; +/** + * Что показать в колонке «Учитывать в КФО» (AURORA-2033, макет 23.10: флажок с подписью «Да»/«Нет»). + * Агрегат учитывается всегда — флажок закреплён (`locked`), подсказка объясняет почему; нераспределённая строка + * переключается, подсказка — причина «только чтение»; у строки ОСВ внутри агрегата флажка нет (`null`). + */ +export type IncludeView = { checked: boolean; locked: boolean; label: 'Да' | 'Нет'; hint: string | null }; + +export function includeView(row: TableRow, readOnlyHint: string | null): IncludeView | null { + if (row.kind === 'aggregate') return { checked: true, locked: true, label: 'Да', hint: AGGREGATE_FLAG_HINT }; + if (row.kind !== 'unallocated') return null; + const checked = !row.entry.excluded; + return { checked, locked: false, label: checked ? 'Да' : 'Нет', hint: readOnlyHint }; +} + /** Тело запроса для флажка «Учитывать в КФО» нераспределённой строки: поставлен — INCLUDE, снят — EXCLUDE. */ export function includeBody(entryIds: string[], side: Side, checked: boolean): AllocationIn { return { statement_type: 'balance', entry_ids: entryIds, side, action: checked ? 'INCLUDE' : 'EXCLUDE' }; -- 2.47.2