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' };