arm-core/docs/superpowers/plans/2026-10-28-corrections-table-ui.md
Andrey Epifancev 753eccbd97 docs(openspec): рулинги R-27, R-28, чек-лист Task 8, уточнения по ревью ветки (#206)
I-4, M-3, M-9, M-10, M-11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:16:14 +04:00

36 KiB
Raw Blame History

Таблица корректировки (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<CorrectionTableOut>; saveCorrection(versionId: string, key: AggregateKey, body: CorrectionIn): Promise<CorrectionAggregateOut>; revokeCorrection(versionId: string, key: AggregateKey, body: CorrectionRevokeIn): Promise<CorrectionAggregateOut>; allocate(versionId: string, body: AllocationIn): Promise<CorrectionTableOut>; 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):

    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<typeof filterTable>, expanded: ReadonlySet<string>): 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):

    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<CorrectionIn, 'debit' | 'credit' | 'comment'> } | { ok: false; errors: Record<string, string> }
                                                                    // ключ ошибки `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):

    type Draft = { state: EditorState; savedAt: string; baseVersionNo: number | null };
    draftKey(userId: string, versionId: string, aggregateKey: string): string  // 'armka:correction-draft:<user>:<version>:<aggregate>'
    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/<id>/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 остаются как тестовые фикстуры — их импортируют тесты моделей.