# Таблица корректировки (UI) — план реализации > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** вкладка «Таблица корректировки» страницы отчёта версии: таблица агрегатов ВГО со строками ОСВ, нераспределённые строки, решения по строкам, окно корректировки агрегата, экспорт — по дизайну 23.10 и аналитике Лысенко. **Architecture:** только фронт `web/`. Данные — по контракту `../docs/dev/api-contract-corrections.md` (+ `.openapi.yaml`); пока бэкенда (AURORA-1540) нет, клиент `services/api/corrections.ts` при `REACT_APP_CORRECTIONS_MOCK=true` отвечает из фейка в памяти. Вся логика (строки таблицы, фильтры, итоги, окно, черновик) — чистые функции с тестами vitest; компоненты тонкие и проверяются в браузере против дизайна. **Tech Stack:** React 18, TS strict, antd 6, react-router 7, vitest 3, biome (+ ESLint/Prettier `npm run lint`). **Spec:** `openspec/changes/corrections-table-ui/` — `design.md` (главное), `proposal.md`, `specs/corrections-table-ui/spec.md`, дельта `specs/intercompany-matrix-ui/spec.md`. Контракт — `/Users/andrey.epifancev/projects/awp-ca/.wt/corrections-ui/docs/dev/api-contract-corrections.md`. Дизайн — `/Users/andrey.epifancev/projects/awp-ca/ДКБ_таблица_корректировок_23_10_26_1.html` (SPA; открывать в браузере). **Ветка:** `feat/206-corrections-ui`, worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/corrections-ui/arm-core`, issue RSHB/arm-core#206, Jira AURORA-1541. ## Global Constraints - Только `web/`; бэкенд, DBML, `api/` не трогать. - Деньги — строки контракта в рублях; показ — `formatScaledAmount` (`utils/money.ts`) через `useNumberDisplay`; сложение — только `sumAmounts`; никаких `Number(...)`/`parseFloat` над суммами. - Фронт не считает разнесение, отклонения, порог и статусы — берёт из ответа (`exceeds_threshold`, `deviation_*`, `status`, `candidate_rule_ids`). Исключение — живой контроль в окне (суммы несохранённых статей) и «Итого» видимых строк. - Только `statement_type = 'balance'`. - Тексты (дословно): вкладка «Таблица корректировки»; «Учитывать в КФО»; группы «Дебет», «Кредит», «Контроль», «Корректировка»; «Отклонение, %», «Отклонение, сумма»; «Корректировать»; «Корректировка операции»; «Исходные данные ОСВ»; «Добавить строку ОСВ»; «Распределение корректировки»; «Добавить строку»; «Комментарий к корректировке»; «Отменить»; «Сохранить корректировки»; «Отображение» — «Общий» / «По пресету»; «Правило»; «Тип записи» — «Все» / «Пары счетов» / «Нераспределенные записи»; «Сбросить фильтры»; «Настройка таблицы»; «Экспорт»; «На весь экран»; «Итого»; «Внутригрупповых остатков между компаниями периметра нет»; «Нет записей для выбранных фильтров»; «Состав изменился после корректировки»; «черновик»; «Перенести в правило…»; «Отнести к правилу…»; «Убрать из правила»; «Вернуть к автоматике»; «Отменить корректировку»; «Да» / «Частично» / «Нет»; «Восстановлены несохранённые правки от ЧЧ:ММ»; «Сбросить»; «Корректировку изменили после ваших правок»; «Есть несохранённые изменения. Закрыть без сохранения?»; «Остаться»; «Корректировку уже изменили, данные обновлены»; подсказки только чтения: `VERSION_NOT_EDITABLE` — «Версия закрыта для изменений», `LOCK_REQUIRED` — «Возьмите консолидацию в работу», `LOCKED_BY_OTHER` — «Консолидацию редактирует другой пользователь». - Подпись правила: `Правило N: <статья 1> — <статья 2>`, составная статья `"1450/1550"` показывается `1450 / 1550`. - Порог для красного в окне (живой контроль): процент к меньшей стороне `≥ 5` или одна сторона 0 при ненулевой другой. - `localStorage`/`sessionStorage` — только в `try/catch`; недоступно — работа без черновика. - Стиль: табы, одинарные кавычки, ширина 140 (biome); комментарии в коде — по-русски; файлы виджета — в `web/src/widgets/CorrectionTable/`. - Проверки перед коммитом задачи: из `web/` — `npm test`, `npx tsc --noEmit -p .`, `npx biome check src`; в Task 7 — ещё `npm run build` и `make web-check` из корня репозитория. ## Review Focus - Ответ на устаревший запрос (аналитик быстро щёлкнул два флажка) перетирает свежую таблицу — `useCorrectionTable` применяет только ответ последнего запроса (счётчик запросов, как `usePagedRows`); тест в Task 4 на функцию выбора ответа. - Сумма статьи «1 000 000,5» / «1000000,567» / «-5» / пусто в окне: принимается только `parseAmountInput`-валидное ≥ 0 с ≤ 2 знаками, иначе поле с ошибкой и «Сохранить» неактивна — тест в Task 3 (`validateLines`). - Черновик из `localStorage` от другой формы данных (старый формат, мусор) не ломает окно — `loadDraft` возвращает `null` — тест в Task 3. - Агрегат исчез из ответа после переноса строк, пока окно открыто (все строки убраны) — окно показывает «—» по сторонам и не падает; `findAggregate` возвращает `null` — тест в Task 2. - Фильтр «ЮЛ» с латиницей/регистром/кавычками («ооо "бск"») находит «ООО "БСК"» — сравнение без регистра — тест в Task 2. --- ### Task 1: Типы контракта, клиент API, флаг фейка **Files:** - Create: `web/src/types/corrections.ts` - Create: `web/src/services/api/corrections.ts` - Modify: `web/src/conf/config.ts` - Test: `web/src/services/api/corrections.test.ts` **Interfaces:** - Produces (`types/corrections.ts`) — типы 1:1 с `components.schemas` OpenAPI: `StatementType`, `Side = 'D' | 'K'`, `Ref`, `CorrectionTableOut`, `CorrectionRuleOut`, `CorrectionAggregateOut`, `AggregateSideOut`, `CorrectionEntryOut`, `UnallocatedEntryOut`, `CorrectionOut`, `CorrectionLineOut`, `CorrectionBalanceOut`, `CorrectionTotalsOut`, `CorrectionLineIn`, `CorrectionIn`, `CorrectionRevokeIn`, `AllocationAction = 'ASSIGN' | 'UNASSIGN' | 'EXCLUDE' | 'INCLUDE' | 'RESET'`, `AllocationIn`, `ReadOnlyReason`, `AggregateKey = { rule_id: string; company_1_id: string; company_2_id: string }`. - Produces (`services/api/corrections.ts`): `getCorrectionTable(versionId: string): Promise`; `saveCorrection(versionId: string, key: AggregateKey, body: CorrectionIn): Promise`; `revokeCorrection(versionId: string, key: AggregateKey, body: CorrectionRevokeIn): Promise`; `allocate(versionId: string, body: AllocationIn): Promise`; `exportCorrectionTable(versionId: string): Promise<{ blob: Blob; filename: string | null }>` (форма ответа — как у `apiFetchBlob`; если она другая — вернуть то, что даёт `apiFetchBlob`, и записать в Interfaces); `correctionsPath(versionId: string, key?: AggregateKey): string`. При `Config.CORRECTIONS_MOCK` все функции делегируют в `correctionsFake` (Task 2); флаг читается в рантайме, фейк остаётся в бандле — это допустимо (данные фейка — небольшие, без персональных данных). - Produces (`conf/config.ts`): `Config.CORRECTIONS_MOCK: boolean` = `environment.REACT_APP_CORRECTIONS_MOCK === 'true'`. - [ ] **Step 1: node_modules в worktree.** `ln -s /Users/andrey.epifancev/projects/awp-ca/arm-core/web/node_modules web/node_modules` (тот же `package-lock.json`, что у `main`; в git не попадает). Проверка: `cd web && npx vitest --version` печатает `3.1.4`. - [ ] **Step 2: Failing test** `corrections.test.ts`: `correctionsPath('v1')` → `'/consolidations/versions/v1/corrections'`; `correctionsPath('v1', {rule_id: 'r', company_1_id: 'a', company_2_id: 'b'})` → `'/consolidations/versions/v1/corrections/aggregates/r/a/b'`. - [ ] **Step 3: Run** `cd web && npx vitest run src/services/api/corrections.test.ts` — FAIL (нет модуля). - [ ] **Step 4: Implement** типы, `correctionsPath`, функции поверх `api.get/put/post` и `apiFetchBlob` (`?statement_type=balance` в GET и export), флаг в `Config`. До Task 2 ветка фейка — `throw new Error('фейк в Task 2')`. - [ ] **Step 5: Run** тест — PASS; `npx tsc --noEmit -p .` и `npx biome check src` — чисто. - [ ] **Step 6: Commit** `feat(web): типы и клиент API таблицы корректировок (#206)`. ### Task 2: Фейк контракта и модель таблицы **Files:** - Create: `web/src/widgets/CorrectionTable/mockData.ts` — данные из дизайна 23.10 - Create: `web/src/services/api/correctionsFake.ts`, Test: `web/src/services/api/correctionsFake.test.ts` - Create: `web/src/widgets/CorrectionTable/tableModel.ts`, Test: `web/src/widgets/CorrectionTable/tableModel.test.ts` - Modify: `web/src/services/api/corrections.ts` (ветка фейка) **Interfaces:** - Consumes: типы Task 1. - Produces (`correctionsFake.ts`): объект `correctionsFake` с теми же сигнатурами, что функции Task 1, состояние в памяти модуля, `resetCorrectionsFake(): void` для тестов. Правила фейка: агрегат пересчитывается из своих строк (суммы сторон `sumAmounts` по не-`excluded`, `deviation_amount`, `deviation_percent` к меньшей с двумя знаками, `exceeds_threshold` при `≥ 5` или одна сторона 0), `composition_changed = true` при изменении состава агрегата с корректировкой; `ASSIGN` — только в правило из `candidate_rule_ids`, иначе `ApiError` 422 `RULE_NOT_APPLICABLE`; пустой комментарий — 422 `COMMENT_REQUIRED`; `expected_version_no` не совпал — 409 `CORRECTION_VERSION_CONFLICT`; `revoke` без корректировки — 409 `CORRECTION_NOT_APPLIED`; `editable = true`. Деление для процента — BigInt-копейки с округлением половины вверх. `mockData.ts`: 8 правил (как `api/seed/core/elimination_rules_v1.yaml`, статьи `1150`/`1450/1550`, `1170`/`1310/1350`, `1170`/`1410`, `1240`/`1510`, `1230`/`1510`, `1230`/`1520`, `1230`/`1410`, `1230`/`1510`), агрегаты и нераспределённые строки из дизайна (суммы в рублях: «ЭПТ — ИЛС» 1000000.00 / 202320.00 с корректировкой 1230 / 1520 и комментарием «Требуется сверка с контрагентом»; «ООО "БЕКОВСКИЙ САХАРНЫЙ ЗАВОД" — БЕКОВСКИЙ САХАРНЫЙ КОМБИНАТ ООО» правила 6 — четыре строки с каждой стороны; правило 3 — «Пензенский Мир Продуктов — Регион-Сахар» 29742550.00 / 29242550.00; нераспределённые — не меньше пяти, включая кредитовую 76.05 «ООО "БСК"» → «ПМП ООО» 142878.00 с `candidate_rule_ids` правил 6 и 7). - Produces (`tableModel.ts`): ```ts type TableRow = | { kind: 'aggregate'; key: string; aggregate: CorrectionAggregateOut; include: 'Да' | 'Частично' | 'Нет' } | { kind: 'entry'; key: string; parentKey: string; side: 1 | 2; entry: CorrectionEntryOut } | { kind: 'unallocated'; key: string; entry: UnallocatedEntryOut }; type DisplayMode = { mode: 'all' } | { mode: 'preset'; ruleId: string }; type RecordType = 'all' | 'posting' | 'unallocated'; type DeviationFilter = 'all' | 'over' | 'under'; type TableFilters = { display: DisplayMode; recordType: RecordType; entity: string; line: string | null; account: string; deviation: DeviationFilter }; const EMPTY_FILTERS: TableFilters; aggregateKey(a: CorrectionAggregateOut): string // `${rule_id}|${company_1.id}|${company_2.id}` toAggregateKey(a: CorrectionAggregateOut): AggregateKey includeState(a: CorrectionAggregateOut): 'Да' | 'Частично' | 'Нет' ruleLabel(r: CorrectionRuleOut): string // «Правило 6: 1230 — 1520», «Правило 1: 1150 — 1450 / 1550» articleCodes(line: string): string[] // '1450/1550' → ['1450','1550'] lineOptions(table: CorrectionTableOut): string[] // отдельные коды статей агрегатов, по возрастанию filterTable(table: CorrectionTableOut, f: TableFilters): { aggregates: CorrectionAggregateOut[]; unallocated: UnallocatedEntryOut[] } buildRows(view: ReturnType, expanded: ReadonlySet): TableRow[] visibleTotals(aggregates: CorrectionAggregateOut[]): { amount_1: string | null; amount_2: string | null; deviation_amount: string | null; correction_debit: string | null; correction_credit: string | null } isFiltered(f: TableFilters): boolean findAggregate(table: CorrectionTableOut, key: string): CorrectionAggregateOut | null ``` Правила `filterTable` — design.md «Панель»: пресет — агрегаты правила + нераспределённые с этим правилом в `candidate_rule_ids`; `recordType`; `entity` — подстрока без регистра в именах обеих сторон (у нераспределённых — `owner`, `counterparty`); `account` — начало кода любого счёта строки (`accounts` сторон / `account_code`); `line` и `deviation` — только агрегаты (по `articleCodes` статей сторон и по `exceeds_threshold`), при любом из них нераспределённые скрываются. - [ ] **Step 1: Failing tests `tableModel.test.ts`** (на `mockData`): `ruleLabel` правила 1 = `'Правило 1: 1150 — 1450 / 1550'`; `articleCodes('1310/1350')` = `['1310','1350']`; `includeState` — все не исключены → `'Да'`, одна из восьми исключена → `'Частично'`, все → `'Нет'`; пресет правила 3 → только агрегаты правила 3 и нераспределённые, где правило 3 в `candidate_rule_ids`; `recordType: 'unallocated'` → `aggregates = []`; `entity: 'ооо "бск"'` → агрегаты и нераспределённые «ООО "БСК"» (Review Focus); `deviation: 'over'` → только `exceeds_threshold`, `unallocated = []`; `buildRows` с раскрытым агрегатом правила 6 «Бековский завод — комбинат» → после агрегата 4 строки `side: 1`, затем 4 `side: 2`; `visibleTotals` по агрегатам «ЭПТ — ИЛС» и правила 3 → `amount_1 = '30742550.00'`; `findAggregate(table, 'нет')` → `null` (Review Focus). - [ ] **Step 2: Failing tests `correctionsFake.test.ts`**: `getCorrectionTable` после `resetCorrectionsFake` отдаёт агрегат «ЭПТ — ИЛС» с `deviation_percent = '394.27'`, `exceeds_threshold = true`; `allocate EXCLUDE` строки 62.03 агрегата правила 6 уменьшает `side_1.amount` на её сумму; `allocate ASSIGN` в правило не из `candidate_rule_ids` — `ApiError` с `code = 'RULE_NOT_APPLICABLE'`; `saveCorrection` с `comment: ' '` — `COMMENT_REQUIRED`; повторный `saveCorrection` с `expected_version_no: null`, когда корректировка есть, — `CORRECTION_VERSION_CONFLICT`; `revokeCorrection` → `correction = null`, затем `saveCorrection` с `expected_version_no: null` проходит. - [ ] **Step 3: Run** `cd web && npx vitest run src/widgets/CorrectionTable src/services/api/correctionsFake.test.ts` — FAIL. - [ ] **Step 4: Implement** `mockData.ts`, `correctionsFake.ts`, `tableModel.ts`; в `corrections.ts` ветка флага вызывает `correctionsFake`. `ApiError` создавать так же, как это делает `api/client.ts` (посмотреть конструктор). - [ ] **Step 5: Run** тесты — PASS; tsc и biome — чисто. - [ ] **Step 6: Commit** `feat(web): фейк контракта корректировок и модель таблицы (#206)`. ### Task 3: Модель окна корректировки и черновик **Files:** - Create: `web/src/widgets/CorrectionTable/editorModel.ts`, Test: `web/src/widgets/CorrectionTable/editorModel.test.ts` - Create: `web/src/widgets/CorrectionTable/draftStorage.ts`, Test: `web/src/widgets/CorrectionTable/draftStorage.test.ts` **Interfaces:** - Consumes: типы Task 1, `articleCodes`, `aggregateKey` (Task 2), `parseAmountInput`, `sumAmounts` (`utils/money.ts`). - Produces (`editorModel.ts`): ```ts type EditLine = { line_code: string; amount: string }; // amount — как ввёл пользователь type EditorState = { debit: EditLine[]; credit: EditLine[]; comment: string }; initialEditorState(a: CorrectionAggregateOut): EditorState // есть корректировка — её статьи (суммы как в API); // нет — по стороне одна статья: первый код articleCodes(article_line), сумма стороны validateLines(s: EditorState): { ok: true; body: Pick } | { ok: false; errors: Record } // ключ ошибки `debit.0.amount`; пустая сторона ок, обе пустые — ошибка «Добавьте статью»; // код — 4 цифры; сумма — parseAmountInput, ≥ 0, ≤ 2 знаков liveControl(a: CorrectionAggregateOut, s: EditorState): { sourceDiff: Diff; allocationDiff: Diff } type Diff = { left: string | null; right: string | null; difference: string | null; percent: string | null; red: boolean } // percent к меньшей стороне, 2 знака; red — percent ≥ 5 или одна сторона 0 при ненулевой другой isDirty(initial: EditorState, current: EditorState): boolean pickerCandidates(table: CorrectionTableOut, a: CorrectionAggregateOut, side: 1 | 2): UnallocatedEntryOut[] // та же пара: side 1 — owner = company_1, counterparty = company_2, entry.side = rule.side_1; // side 2 — наоборот и противоположная сторона; правило агрегата в candidate_rule_ids; не excluded toCorrectionIn(body: …, expectedVersionNo: number | null): CorrectionIn // status 'APPLIED' ``` - Produces (`draftStorage.ts`): ```ts type Draft = { state: EditorState; savedAt: string; baseVersionNo: number | null }; draftKey(userId: string, versionId: string, aggregateKey: string): string // 'armka:correction-draft:::' saveDraft(key: string, draft: Draft, storage?: Storage | null): void loadDraft(key: string, storage?: Storage | null): Draft | null // мусор/старый формат/исключение — null clearDraft(key: string, storage?: Storage | null): void isStale(draft: Draft, a: CorrectionAggregateOut): boolean // baseVersionNo ≠ (a.correction?.version_no ?? null) ``` `storage` по умолчанию — `window.localStorage`, доступ в `try/catch`. - [ ] **Step 1: Failing tests `editorModel.test.ts`**: `initialEditorState` агрегата правила 3 без корректировки → `debit = [{line_code: '1170', amount: '29742550.00'}]`, `credit = [{line_code: '1410', amount: '29242550.00'}]`; агрегат правила 1 без корректировки → кредит `line_code = '1450'`; `validateLines` с суммами `'1 000 000,5'`, `'1000000,567'`, `'-5'`, `''` → ошибки у `amount` (Review Focus), с `'500000,5'` → `'500000.50'` в `body`; `liveControl` агрегата «ЭПТ — ИЛС» с исходным состоянием → `sourceDiff.percent = '394.27'`, `red = true`; при Дт 1000000.00 и Кт 1000000.00 распределения → `allocationDiff.difference = '0.00'`, `red = false`; `pickerCandidates` для агрегата «Пензенский Мир Продуктов — Бековский сахарный комбинат» правила 6, сторона 2 → только кредитовые нераспределённые строки комбината на ПМП с правилом 6. - [ ] **Step 2: Failing tests `draftStorage.test.ts`** (`storage` — простая заглушка `Map` с `getItem/setItem/removeItem`): `saveDraft` → `loadDraft` возвращает то же; `setItem` строки `'{"x":1}'` → `loadDraft` = `null` (Review Focus); заглушка, бросающая на `getItem`, → `null` без исключения; `isStale` при `baseVersionNo = 2` и `correction.version_no = 3` → `true`. - [ ] **Step 3: Run** `cd web && npx vitest run src/widgets/CorrectionTable` — FAIL. - [ ] **Step 4: Implement** оба модуля. - [ ] **Step 5: Run** — PASS; tsc и biome — чисто. - [ ] **Step 6: Commit** `feat(web): модель окна корректировки и черновик (#206)`. ### Task 4: Вкладка и таблица только на чтение **Files:** - Create: `web/src/widgets/CorrectionTable/useCorrectionTable.ts`, Test: `web/src/widgets/CorrectionTable/useCorrectionTable.test.ts` - Create: `web/src/widgets/CorrectionTable/CorrectionTable.tsx` (таблица), `CorrectionToolbar.tsx` (панель), `CorrectionContent.tsx` (вкладка: загрузка, состояния, панель + таблица) - Modify: `web/src/widgets/ConsolidatedOsv/OsvTabs.tsx` (у `correction` убрать `disabled`/`tooltip`) - Modify: `web/src/layouts/ConsolidationReportPage.tsx` (`TAB_COMPONENTS.correction`, `isValidTab` с `correction`) **Interfaces:** - Consumes: Task 1–2 (`getCorrectionTable`, `filterTable`, `buildRows`, `visibleTotals`, `ruleLabel`, `lineOptions`, `isFiltered`, `EMPTY_FILTERS`), `useNumberDisplay`, `ColumnSettings`/`useVisibleColumns` (`components/GenericTable`), полноэкранный режим — как `toggleFullscreen` в `ConsolidatedChessContent.tsx`. - Produces: `latestOnly(): { next(): number; isLatest(id: number): boolean }` (в `useCorrectionTable.ts`, чистая, тестируется); `useCorrectionTable(versionId: string): { table: CorrectionTableOut | null; loading: boolean; error: string | null; reload(): void; replace(t: CorrectionTableOut): void; replaceAggregate(a: CorrectionAggregateOut): void }` — применяет только ответ последнего запроса (Review Focus); `CorrectionContent({ versionId }: { versionId: string })`; `CorrectionTable` props: `{ rows: TableRow[]; totals; format; expanded; onToggle(key); rules; editable; readOnlyHint: string | null; actions: RowActions }`, где `RowActions` — колбэки Task 5–6 (`onCorrect(a)`, `onRevoke(a)`, `onAllocate(body)`), в этой задаче — заглушки. - [ ] **Step 1: Failing test** `useCorrectionTable.test.ts`: `latestOnly` — после `next()` = 1 и `next()` = 2 `isLatest(1) = false`, `isLatest(2) = true`. - [ ] **Step 2: Run** — FAIL. - [ ] **Step 3: Implement** хук, вкладку и таблицу: колонки и тексты — Global Constraints и spec «Колонки и строка агрегата», «Раскрытие», «Нераспределённые строки», «Панель и фильтры»; суммы — `format` из `useNumberDisplay` (по умолчанию «тыс. руб.», 2 знака, как в дизайне); красное — при `exceeds_threshold`; `composition_changed` — фон строки и значок с подсказкой; `DRAFT` — метка «черновик»; «Итого» — `visibleTotals`, процент «—»; флажки и меню пока неактивны. Экспорт — `exportCorrectionTable` + `saveBlob(blob, filename ?? xlsxFilename('Таблица_корректировки', …))`. Вкладка доступна, адрес `…/correction`. - [ ] **Step 4: Run** тест — PASS; `npx tsc --noEmit -p .`, `npx biome check src` — чисто. - [ ] **Step 5: Проверка в браузере** (фейк): `cd web && REACT_APP_CORRECTIONS_MOCK=true npm run start`, бэкенд ядра для карточки версии — как в `../docs/dev/qa-guide.md`; открыть `…/versions//correction`; сверить с дизайном (открыть `ДКБ_таблица_корректировок_23_10_26_1.html` рядом): колонки, раскрытие агрегата правила 6, нераспределённые строки, пресет правила 3, фильтры, «Итого», единицы. Снимки — в `/private/tmp/claude-501/` (scratchpad сессии), в репозиторий не класть. - [ ] **Step 6: Commit** `feat(web): вкладка «Таблица корректировки» — таблица и панель (#206)`. ### Task 5: Решения по строкам, отмена корректировки, только чтение **Files:** - Create: `web/src/widgets/CorrectionTable/RowActions.tsx` (флажок «Учитывать в КФО», меню строки, выбор правила) - Create: `web/src/widgets/CorrectionTable/RevokeModal.tsx` - Create: `web/src/widgets/CorrectionTable/readOnly.ts`, Test: `web/src/widgets/CorrectionTable/readOnly.test.ts` - Modify: `CorrectionTable.tsx`, `CorrectionContent.tsx` **Interfaces:** - Consumes: `allocate`, `revokeCorrection` (Task 1), `replace`/`replaceAggregate`/`reload` (Task 4), `ruleLabel`. - Produces: `readOnlyHint(reason: ReadOnlyReason | null): string | null` (тексты — Global Constraints); `allocationMenu(row: TableRow, rules: CorrectionRuleOut[]): { key: AllocationAction | 'MOVE'; label: string; disabled: boolean }[]` (тестируется: у строки агрегата с `decision = 'AUTO'` нет «Вернуть к автоматике»; пустой `candidate_rule_ids` — «Перенести в правило…» неактивен; у нераспределённой `reason = 'UNASSIGNED'` есть «Вернуть к автоматике»). - [ ] **Step 1: Failing tests** `readOnly.test.ts`: `readOnlyHint('LOCK_REQUIRED')` = «Возьмите консолидацию в работу», `null` → `null`; три случая `allocationMenu` выше. - [ ] **Step 2: Run** — FAIL. - [ ] **Step 3: Implement**: флажок строки → `allocate({statement_type: 'balance', entry_ids, action: checked ? 'INCLUDE' : 'EXCLUDE'})`, ответ — `replace`; меню → `ASSIGN` (выбор правила из `candidate_rule_ids`, подписи `ruleLabel`), `UNASSIGN`, `RESET`; «Отменить корректировку» в меню агрегата → `RevokeModal` (комментарий обязателен, пустой — «Сохранить» неактивна) → `revokeCorrection` → `replaceAggregate`. Ошибки API — уведомление с `ApiError.message`; 409 правок версии — ещё `reload()`. `editable = false` — всё неактивно с подсказкой `readOnlyHint`. - [ ] **Step 4: Run** тесты — PASS; tsc, biome — чисто. - [ ] **Step 5: Проверка в браузере** (фейк): исключить строку 62.03 → у агрегата «Частично», сумма стороны меньше; «Отнести к правилу…» у кредитовой 76.05 «ООО "БСК"» → строка в агрегате; «Отменить корректировку» у «Сатурн — Архол». Флаг фейка `editable = false` (временно в тесте руками) — подсказки. - [ ] **Step 6: Commit** `feat(web): решения по строкам и отмена корректировки (#206)`. ### Task 6: Окно «Корректировка операции» **Files:** - Create: `web/src/widgets/CorrectionTable/CorrectionEditorModal.tsx` - Modify: `CorrectionContent.tsx` (открытие окна из «Корректировать») **Interfaces:** - Consumes: Task 3 целиком, `allocate`, `saveCorrection`, `findAggregate`, `toAggregateKey`, `useCurrentUser().id`, `replace`/`replaceAggregate`/`reload`. - Produces: `CorrectionEditorModal({ versionId, table, aggregateKey, editable, onClose, onTable, onAggregate })`. - [ ] **Step 1: Implement** по spec «Окно…», «Сохранение…», «Черновик окна…»: состояние — `initialEditorState` или черновик (`loadDraft(draftKey(...))`, плашки «Восстановлены несохранённые правки от ЧЧ:ММ» / «Корректировку изменили после ваших правок» по `isStale`, кнопка «Сбросить»); `saveDraft` при каждой правке; «×» исходной строки и «Добавить строку ОСВ» (`pickerCandidates`) — `allocate` сразу, таблица — `onTable`, агрегат окна — `findAggregate` (нет — по сторонам «—», Review Focus); контроль — `liveControl`; «Сохранить корректировки» — `validateLines` → `saveCorrection(toCorrectionIn(body, aggregate.correction?.version_no ?? null))` → `clearDraft`, `onAggregate`, закрыть; ошибки: `COMMENT_REQUIRED` — у комментария, `INVALID_LINE_CODE` — у статьи `detail.line_code`, `CORRECTION_VERSION_CONFLICT` — сообщение «Корректировку уже изменили, данные обновлены» + `reload()`; «Отменить» при `isDirty` — `Modal.confirm` «Есть несохранённые изменения. Закрыть без сохранения?» («Остаться» / закрыть с `clearDraft`). `editable = false` — всё только на просмотр, без «Сохранить корректировки». - [ ] **Step 2: Run** `npx tsc --noEmit -p .`, `npx biome check src`, `npm test` — чисто. - [ ] **Step 3: Проверка в браузере** (фейк): окно «ЭПТ — ИЛС» (суммы, 394,27 %, красное); правило 3 без корректировки — подстановка 1170 / 1410; добавить статью 1550 на стороне Кт, сохранить без комментария → ошибка у поля; с комментарием → таблица показывает две статьи; правка суммы → «Назад» в браузере → вернуться → плашка восстановления; «Отменить» → вопрос. Сверка вида окна с дизайном (`editor0.png`, `editor-unalloc.png` — снимки в `/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/d22b6541-bb55-46fc-a4fe-8bba06387616/scratchpad/mockup-1023/`). - [ ] **Step 4: Commit** `feat(web): окно «Корректировка операции» с черновиком (#206)`. ### Task 7: Сверка с дизайном, проверки, документация **Files:** - Modify: `web/README.md` (раздел про флаг `REACT_APP_CORRECTIONS_MOCK`), `web/ARCHITECTURE.md` (виджет `CorrectionTable`) - Modify: `openspec/changes/corrections-table-ui/tasks.md` (создать по факту выполненного, все пункты отмечены) - [ ] **Step 1:** полный проход сценариев spec `corrections-table-ui` в браузере на фейке, сверка каждой части экрана с дизайном в Playwright (оба окна рядом, одинаковая ширина 1920); расхождения — исправить или записать в отчёт с причиной. - [ ] **Step 2:** из корня репозитория `make web-check` — PASS; из `web/` — `npm test`, `npm run lint` — PASS. - [ ] **Step 3:** `openspec validate corrections-table-ui --strict` — valid. - [ ] **Step 4: Commit** `docs(web): таблица корректировок — README, ARCHITECTURE, tasks (#206)`. ### Task 8 (после слияния AURORA-1540): подключение к API Выполняется отдельно, когда эндпоинты 1540 в `main`. Влить `main` в ветку; запустить без флага на стенде (`../docs/dev/qa-guide.md`) на реальной версии; пройти сценарии spec; расхождения ответа с контрактом — правка в фейке и типах или замечание в PR 1540; `make check-full`; PR в `main` (до этого PR не открывать — фронт без API не сливаем, если владелец не решит иначе). Уточнение (R-27): в Task 8 удаляются только `services/api/correctionsFake.ts` (+ `correctionsFake.test.ts`) и ветка флага `CORRECTIONS_MOCK` в `corrections.ts`/`config.ts`; `widgets/CorrectionTable/mockData.ts` и `mockCalc.ts` остаются как тестовые фикстуры — их импортируют тесты моделей. ### Task 9 (29.10, вне исходного плана): ответы Лысенко и WIP API 1540 Лысенко ответил на `[design-23-10-check]` (AURORA-1395, #9920570), Илья выложил WIP API (arm-core #208). Сделано сессией сразу, без SDD: отзыв корректировки и `status` в `PUT` убраны; «Учитывать в КФО» — только у нераспределённых, агрегат закреплён; у нераспределённой — строка баланса владельца `line_code`; фейк отвечает `ENTRY_ALLOCATED` / `ENTRY_EXCLUDED`. Пункты — `openspec/changes/corrections-table-ui/tasks.md` §9, рулинги R-29…R-34 в `design.md`. Тексты «Отменить корректировку» и «Да» / «Частично» / «Нет» из Global Constraints больше не используются. Контракт docs — отдельный PR.