diff --git a/CLAUDE.md b/CLAUDE.md index 91fe069..bbe34c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,14 @@ SPA кредитного аналитика АРМ КА: справочники - `src/store/` — Redux Toolkit: `store.ts`, слайсы `companyGroupsSlice` и `consolidations` (+ `thunks.ts`). - `src/layouts/` — страницы (маршруты в `src/App.tsx`, в т.ч. `/documents/:document_id` и `/groups/:group_id/versions/:version_id/osv`), `src/widgets/` — блоки экранов (`DocumentReview` — проверка ББ/ОФР/ОСВ со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer` — файлы ЮЛ (стол консолидации / все файлы ЮЛ, подпись консолидации, селектор `store/slices/consolidations/selectors.ts`)), `src/components/` — примитивы (`GenericTable`, `TableTag`, `PageTitle`), `src/types/` — DTO API. +- Экран проверки формы (`src/widgets/DocumentReview/`): вся логика — чистые модули с тестами рядом, компоненты `.tsx` без юнит-тестов. + `formLayout` — раскладка строк по разделам и статусам; `issuePlaces` — предупреждения по местам (ячейка / колонка / строка / поле шапки), + список для панели и `collapseIssues`; `statementDraft` — весь черновик правки и сборка тела `PATCH` (суммы, подтверждения, коды, даты, шапка); + `cellView` — цвета, подсказки, текст ячейки суммы и кода; `codeSuggestions` — подсказки кодов по справочнику; `lineMenu` — меню строки + («Это расшифровка», «Назначить код»); `editRules` — что можно править и какая кнопка подтверждения нужна ячейке/колонке; + `confirmBlockers` — строки без кода, блокирующие подтверждение; `focusTarget` — к какой строке прокрутить по «Показать». + `src/widgets/FilesDrawer/fileWarnings` и `documentRows` — предупреждения файла и документа в панели файлов; `src/utils/percent` — + формат уверенности. ## Команды - `make install` — `npm install`. diff --git a/README.md b/README.md index e287afe..c4e24fe 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,15 @@ SPA кредитного аналитика поверх API `arm-core` (`/api/v Если на стенде раньше вручную сопоставляли контрагентов (K14), алиасы сохраняются между версиями, и «несопоставленных» будет меньше (на стенде прогона — 1349); остальные цифры не меняются. +## Экран проверки формы ББ/ОФР (контракт 2.0, ядро v5.19) + +Что нового (#43): строки формы показываются по статусам (`ok` / нераспознанный код / без кода / расшифровка) на своих местах; +предупреждения и нарушения стоят значками у ячеек, колонок, строк и полей шапки, а полный список свёрнут до трёх записей +(«Показать все»); в режиме правки можно назначить код строке, пометить её расшифровкой и подтвердить значение, код или дату +колонки — всё уходит одним `PATCH`; кнопка «Подтвердить данные» заблокирована, пока есть строки без кода (ссылка +«Разберите строки без кода: N» ведёт к первой); панель файлов ЮЛ показывает счётчик предупреждений файла и документа. +Карта модулей — в `CLAUDE.md`, раздел «Архитектура». + ## Ограничения прототипа - Нет блокировки версии («Взять в работу»), сопоставления контрагентов, экрана чек-листа, комментариев, удаления файлов, diff --git a/docs/superpowers/plans/2026-10-18-statements-v2.md b/docs/superpowers/plans/2026-10-18-statements-v2.md new file mode 100644 index 0000000..16b01f4 --- /dev/null +++ b/docs/superpowers/plans/2026-10-18-statements-v2.md @@ -0,0 +1,933 @@ +# План 6 — arm-web: экран формы ББ/ОФР и карточка файла по API v5.19. План реализации + +> **Для исполнителей-агентов:** обязательный навык — superpowers:subagent-driven-development (в плане 12 задач, задачи +> опираются на интерфейсы друг друга). Контроллер SDD — фоновый сабагент на модели **sonnet**; исполнители — по SDD Model +> Selection (`~/.claude/CLAUDE.md`, «Исполнение»): T1 — haiku (типы и фикстура заданы планом), остальные — sonnet, +> финальное ревью ветки — opus. Шаги размечены чекбоксами (`- [ ]`). + +**Goal:** экран проверки формы ББ/ОФР [41] показывает все строки источника на своих местах (строки без кода — под +ближайшей предыдущей строкой с кодом, расшифровки — под переключателем), подсвечивает только то, что требует проверки +(`needs_review`), показывает предупреждения и нарушения у места, даёт в режиме «Редактировать» разобрать строки, +подтвердить ячейки, коды и даты одним `PATCH`, блокирует «Подтвердить данные» при неразобранных строках; карточка файла +читает `job.warnings` структурами. + +**Architecture:** вся логика — чистые функции в `.ts` рядом с компонентами (`src/widgets/DocumentReview/`, +`src/widgets/FilesDrawer/`), покрытые vitest; компоненты React только рисуют то, что вернули функции, и не тестируются +юнит-тестами (соглашение `arm-web/CLAUDE.md`). Черновик правок аналитика — одна структура `StatementDraft`, из которой +одна функция собирает тело единственного `PATCH`. Экран проверяется сценарием в Playwright на локальном стенде; до того +как сервисы распознавания начнут слать контракт 2.0, строки со статусами подставляются в ответ `GET …/statement` +перехватом запроса (фикстура плана). + +**Tech Stack:** React 18 + TypeScript strict, antd 6 (`Table`, `AutoComplete`, `Dropdown`, `Popover`, `Tooltip`, +`Switch`), rsbuild, vitest **3.1.4**, biome + ESLint + Prettier; Playwright (MCP-плагин сессии) для сценария; `tea` +(Gitea), OpenSpec. + +**Spec:** `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/openspec/changes/unified-statement-normalization/` +(ветка `feat/116-unified-normalization` репозитория arm-ka-docs): главное — `specs/arm-web/spec.md`; API, которое экран +потребляет, — `specs/arm-core/spec.md` (требования «Ответ `GET …/statement` по v2.0», «PATCH формы — строки, колонки, +подтверждение, наименование организации», «Подтверждение формы — порядок 409», «Предупреждения и нарушения документа — +`document_issue`», «Предупреждения файла и ОСВ в API»); гайд фронта — `dev/frontend-api-guide.md` §5.6.1; правила — +`context/05-domain-rules.md` §9.5 п. 2, §11.9 (таблица предупреждений: коды и уровни); точные схемы ответов и коды — +план 3 (`plans/2026-10-18-plan-3-arm-core.md`), задачи A5, A8, A11, A12, A13; решения — `design.md` (Р-14 экран, Р-23 +значения `detail`, Р-25 подтверждение без правки, S-62 родитель `detail`, S-47 пробелы PATCH). Макет — `mockup/screens/41-document-check.png`, +`41-02-document-check-edit.png`, `39-org-documents.png`. + +--- + +## Место в работе и порядок + +| План | Что | Связь с этим планом | +|---|---|---| +| 3, часть А | arm-core: схема v5.19, `StatementOut` с новыми полями, `PATCH` (`lines`, `columns`, `confirmed`, `organization_name`), порядок 409, `JobBrief.warnings` структурами | задачи T1–T10 пишутся по спеке и фикстуре **до** слияния А; T11 (стенд) — после слияния А в `main` arm-core | +| 3, часть Б | приём контракта 2.0, §7 в `document_issue` | для T11 желательна (нарушения §7 в `issues` у форм 1.x появляются с Б) | +| сервисы (шаг 5 `proposal.md`) | tabular-parser и scan-gateway шлют 2.0 | до них реальных строк `unreadable` / `missing` / `detail` на стенде нет — их показывает перехват (P6-14); повторный прогон на `aep_3` после шага 5 — follow-up | +| `sync-web` | перенос `arm-web@test` в `arm-core/web` | после merge PR этого плана в `test`; снимок контура — только вместе с ядром А + Б (O-9 плана 3) | + +**Выкладка:** фронт выходит в той же выкладке, что ядро: `FileOut.job.warnings` меняет тип со строк на объекты +(несовместимо), а встроенный фронт контура — `arm-core/web`, который обновляется только `make -C docs sync-web`. + +## Как это устроено — для владельца + +- **Раскладка «как на бумаге».** Ядро отдаёт строки списком; разделы баланса и места итогов строит фронт + (`formLayout.ts`, так уже работает с Среза 3b). Новое: строки без кода (`unreadable`, `missing`) и расшифровки + (`detail`) не участвуют в раскладке по коду, а «прицепляются» к строке с кодом — к той, что стояла перед ними в файле + (`row_index`), расшифровка — к своему родителю (`parent_row_index`). Это отдельная чистая функция, её проверяют тесты + на сценариях спеки («ИЧ,250 под 1240»). +- **Почему черновик, а не запрос на каждое действие.** Спека требует: «Сохранить» отправляет **один** `PATCH`. Поэтому + всё, что аналитик делает в режиме «Редактировать» (суммы, подтверждения ячеек, коды строк, даты колонок, шапка), + копится в одном объекте `StatementDraft`, а функция `buildStatementPatch` превращает его в тело запроса и заранее + ловит ошибки, которые иначе вернул бы сервер (неверный формат кода или даты, конфликт «шаблонная строка + тот же код»). + Ответ сервера целиком заменяет состояние экрана — так пересчитанные ядром статусы и проверки всегда актуальны. +- **Почему тесты только на функции.** Так принято в arm-web: компоненты не тестируются, а всё, что решает «что + показать», вынесено в `.ts`-функции. Компоненты проверяются руками в браузере по сценарию T11 со сверкой с макетом. +- **Новых npm-пакетов нет:** всё нужное (выпадающая подсказка кода, меню строки, всплывающий список) есть в antd; в + зеркале npm банка не всё доступно (vitest ≤ 3.1.4). + +## Разбивка на PR + +| | | +|---|---| +| Issue | RSHB/arm-web: «Единая нормализация ББ/ОФР: экран формы и карточка файла по API v5.19 (arm-ka-docs#116)» | +| Ветка | `feat/43-statements-v2` (база — `origin/test`) | +| Зеркальный workspace | `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization-web/` | +| Worktree arm-web | `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization-web/arm-web` | +| Change OpenSpec arm-web | `openspec/changes/statements-v2/` | +| PR | в `test` (не в `main`); merge — только по команде владельца | +| После merge | `make -C /Users/andrey.epifancev/projects/awp-ca/docs sync-web` → PR в arm-core (`arm-core/web` напрямую не править) | + +## Global Constraints + +- Типы `src/types/api.ts` повторяют схемы ядра v5.19 (спека `arm-core` «Ответ `GET …/statement` по v2.0», план 3 A5/A8) + и гайд §5.6.1; полей, которых там нет, не придумывать. +- Все запросы — через `src/api/client.ts` (`api.get|post|patch`); прямого `fetch` нет. +- Суммы — строки (`string | null`); арифметики над суммами на фронте нет; `Number()` — только для уверенности (0..100). +- Подсветка ячейки «требует внимания» — **только** по `values[].needs_review`; по `confidence < 100` и + `is_recognized = false` напрямую не подсвечивать; «исправлено» (`is_modified`) — прежний синий. +- Тексты интерфейса — дословно: «Показать расшифровки (N)», «Разберите строки без кода: N», «Это расшифровка», + «Назначить код», «Сделать расшифровкой», «Подтвердить код», «Подтвердить данные», «Редактировать», «Сохранить». +- Переключателя «как в файле» нет (Р-14). +- «Сохранить» отправляет ровно один `PATCH /documents/{id}/statement` (`header`, `values`, `columns`, `lines`); пустой + черновик — запроса нет; ответ заменяет `StatementOut` целиком. +- Коды ошибок — дословно: 409 `LINES_UNRESOLVED` (`detail.lines[] {line_id, row_index, line_code_text}`), `DUPLICATE_FORM` + (`detail.document_id`), `LINE_CODE_TAKEN` (`detail.line_id`, `detail.line_code`), `PERIOD_REQUIRED`, + `CHECKS_NOT_PASSED` (`detail.failures`); 422 `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, + `LINE_CODE_REQUIRED` (у всех четырёх — `detail.line_id`, `detail.line_code` первого неверного элемента, план 3 + P3-24), `UNKNOWN_COLUMN`, `UNKNOWN_CELL`. +- Новых зависимостей npm нет; `vitest` остаётся `3.1.4`; `package-lock.json` в git не попадает. +- Компоненты (`.tsx`) юнит-тестами не покрываются; каждая чистая функция плана — в `.ts` с `.test.ts` рядом. +- Перед коммитом — `npm run format`; зелёным считается `make lint typecheck test build` (P6-18); перед PR — то же, вывод + в описание PR. +- Язык комментариев, коммитов, issue, PR — русский; идентификаторы — английские, как в API. +- Коммиты заканчиваются строками `Co-Authored-By: Claude Opus 5.5 ` и `Claude-Session: + https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W`; описание PR — строкой `🤖 Generated with [Claude + Code](https://claude.com/claude-code)`. +- Находки и решения — с маркерами (`T.`, `C-n`/`I-n`/`M-n`, `R-n`, `O-n`, `H-n`); решения этого плана — `P6-n` + (ниже); исполнитель переносит их в `openspec/changes/statements-v2/design.md` arm-web, раздел «Decisions made without + discussion». +- Каждый экран после правок сверяется с PNG макета (`docs/mockup/screens/41-*.png`, `39-*.png`) при окне 1600×1000 и + прогоняется самим исполнителем в браузере (память владельца «сверять фронт с макетом»). + +## Review Focus + +1. **Якорь строки без кода, который скрыт или повторён** — строка `unreadable` после пустой пятизначной подстроки + (скрыта по умолчанию), после второй из двух строк 2410, после строки в «Прочих строках»: вложение стоит под своим + экземпляром (по ключу строки, не по коду) и не исчезает, когда якорь скрыт. Тесты — T2. +2. **Предупреждение у скрытой или несуществующей строки** — запись `issues` у строки `detail` при выключенном + переключателе (ссылка «Показать» включает расшифровки и ведёт к строке); `line_ids` со строкой, которой нет в ответе + (удалённая шаблонная), и поле шапки, которого нет в блоке шапки (`okpo`, `okei`), — уходят в список документа, а не + теряются. Тесты — T4. +3. **Смешанный черновик в одном сохранении** — сумма и подтверждение одной ячейки, действие строки и сумма той же + строки, сумма в шаблонную строку 1250 и назначение 1250 строке без кода, пустое поле кода, код, равный текущему: + тело `PATCH` не нарушает правил ядра (одно действие на строку, `UNKNOWN_CELL` по P3-10), пустое не отправляется. + Тесты — T6. +4. **Форма, принятая по 1.x** (все строки `ok`, `row_index` по порядку, `source_ref = null`, у чистых значений + `needs_review = false`, в `issues` — `MAPPING_WARNING`) — экран выглядит как до изменения, кроме подсветки по + `needs_review`; переключателя расшифровок нет (N = 0); кнопка «Подтвердить данные» не заблокирована. Тесты — T2, + T9; сценарий — T11. +5. **`job.warnings` старого формата** — строки, смесь строк и объектов, не список, объект без `severity`: панель файлов + не падает, строки показываются как `LEGACY_WARNING` `info`, мусор пропускается. Тесты — T10. + +--- + +## Карта файлов (arm-web, относительно корня репозитория) + +| Файл | Ответственность | +|---|---| +| `src/types/api.ts` | DTO v5.19: строки, значения, колонки, шапка, `issues`, `JobBrief.warnings`, тела `PATCH` | +| `src/utils/labels.ts` | подписи статуса строки и уровня предупреждения | +| `src/utils/dates.ts` | проверка даты `ГГГГ-ММ-ДД` | +| `src/widgets/DocumentReview/__fixtures__/statement-v2.json` | эталонный `StatementOut` v2 для тестов и перехвата в Playwright | +| `src/widgets/DocumentReview/statementGrid.ts` | `StatementOut` → строки сетки `GridRow` с полями v2 | +| `src/widgets/DocumentReview/cellView.ts` | вид ячейки значения и ячейки кода: фон, подсказка, приглушённый текст | +| `src/widgets/DocumentReview/formLayout.ts` | раскладка по разделам + вложения (`unreadable` / `missing` / `detail`), фильтр показа, имена | +| `src/widgets/DocumentReview/issuePlaces.ts` | привязка `issues` к месту (строка, ячейка, колонка, поле, документ), пункты списка | +| `src/widgets/DocumentReview/IssuesPanel.tsx` | список предупреждений документа со ссылкой «Показать» | +| `src/widgets/DocumentReview/headerFields.ts`, `StatementHeaderBlock.tsx` | шапка: наименование организации, `needs_review_fields`, подсказки из `header.source` | +| `src/widgets/DocumentReview/statementDraft.ts` | черновик правок и сборка тела `PATCH` (заменяет `draftToPatch`) | +| `src/widgets/DocumentReview/codeSuggestions.ts`, `lineMenu.ts` | подсказка кода из `edition_lines`, пункты меню строки, ошибка элемента `lines[]` у поля строки | +| `src/widgets/DocumentReview/editRules.ts` | когда у колонки поле даты и у ячейки кнопка подтверждения | +| `src/widgets/DocumentReview/CodeCell.tsx`, `LineMenu.tsx`, `ValueCell.tsx`, `ColumnHeader.tsx` | ячейки таблицы и заголовок колонки | +| `src/widgets/DocumentReview/StatementTable.tsx`, `StatementReview.tsx`, `ChecksPanel.tsx` | сборка экрана, переключатели, сохранение, переход к строке | +| `src/widgets/DocumentReview/confirmBlockers.ts`, `confirmError.ts` | неразобранные строки, разбор 409 подтверждения | +| `src/widgets/DocumentReview/OsvDocument.tsx` | `DocumentOut.issues` у ОСВ-документа | +| `src/widgets/FilesDrawer/fileWarnings.ts`, `documentRows.ts`, `FilesList.tsx`, `FileWarningsPopover.tsx` | предупреждения файла в панели файлов | + +--- + +## Решения плана без обсуждения (`P6-n`) + +| Маркер | Решение | Обоснование | Цена ошибки | +|---|---|---|---| +| P6-1 | Подсказка кода — `StatementOut.edition_lines` (справочник строк редакции формы, у формы без даты — всех редакций; план 3 P3-22), затем коды строк `ok` файла, которых в справочнике нет (имя — `reference_name` ?? `line_name`); отдельного запроса нет | справочник приходит с ответом формы | нет | +| P6-2 | Якорь строки `unreadable` / `missing` — строка `ok` из источника (`row_index` не `null`) с наибольшим `row_index` меньше своего; якорь `detail` — строка `ok` из источника с `row_index = parent_row_index`; нет якоря (в том числе `detail` с `parent_row_index = null` или указывающим не на `ok`) — в начало первого раздела (после ведущих заголовков разделов; у ОФР — в начало таблицы). Вложение встаёт сразу после строки-якоря; несколько вложений одного якоря — по `row_index` | спека `arm-web` «Раскладка», S-62 («строка `detail` без родителя на экране — в начале раздела»), P3-17 (тот же порядок вложений в ответе ядра) | нет | +| P6-3 | Скрытие (пустые подстроки, расшифровки) применяется **после** раскладки, фильтром | иначе скрытый якорь сдвинул бы вложения к соседу | нет | +| P6-4 | Справочное имя (`reference_name`) — основное; файловое (`line_name`, после срезания приклеенных заголовков разделов) показывается вторым серым текстом, только если отличается после нормализации: нижний регистр, `ё` → `е`, оставлены только буквы и цифры | переносы OCR («задолжен-ность») не должны давать ложного «отличается» | лишнее или пропущенное второе имя — только показ | +| P6-5 | Маркеры «у места» строятся из **всех** записей `issues` (в том числе `relation` / `soft` — у них `line_ids` + `column_id`); список `IssuesPanel` показывает только `kind = warning`; нарушения §7 остаются списком в `ChecksPanel` (из `checks`, с акцептом) и получают ссылку «Показать» по `line_ids[0]`. Запись с `column_id` без `line_ids` (например, `COLUMN_WITHOUT_DATE`) показывается у заголовка колонки; поле шапки, которого нет в блоке шапки, и строки, которых нет в ответе, — у документа | одна запись не дублируется в двух списках; заголовок колонки — естественное место для предупреждения о дате колонки | нет | +| P6-6 | Подтверждение ячейки — в черновике; доступно, только если у строки есть `id` и у значения есть `id` (у достроенных ячеек — нет, ядро ответит `UNKNOWN_CELL`); сумма в черновике той же ячейки отменяет её подтверждение | `PATCH`: в элементе `values` ровно одно из `amount` / `confirmed`; правка суммы и так снимает `needs_review` | нет | +| P6-7 | Одно действие на строку в черновике (`code` / `detail` / `confirm`), новое заменяет прежнее; пустое поле кода и код, равный текущему, — не действие; у достроенных шаблонных строк (`id = null`) действий строки нет | 422 `INVALID_LINE_PATCH` при двух действиях; `lines[]` адресуется только `line_id` | нет | +| P6-8 | Сумма в шаблонную строку по `line_code` X и назначение кода X другой строке в одном сохранении — ошибка черновика «Код X назначается строке, а в шаблонную строку X введены суммы — сохраните по очереди», запрос не уходит | ядро ответит 422 `UNKNOWN_CELL` (P3-10), а сообщение ядра не говорит, что делать | нет | +| P6-9 | Ошибка элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `LINE_CODE_REQUIRED`) показывается у поля кода строки `detail.line_id` (текст сервера в подсказке поля и `message.error`); строки с этим `id` на экране нет — текстом над таблицей | ядро отдаёт `detail.line_id` и `detail.line_code` (план 3 P3-24) | нет | +| P6-10 | Строка «неразобранная» для кнопки «Подтвердить данные» — `line_status ∈ {unreadable, missing}` и хотя бы у одного значения `amount !== null` или `string_value !== null` (ровно условие ядра для 409 `LINES_UNRESOLVED`) | кнопка и сервер считают одинаково | нет | +| P6-11 | Подтверждение даты отчёта — кнопка «Подтвердить дату» у поля из `needs_review_fields` (уходит `header.report_date` тем же значением); дата колонки — поле `ГГГГ-ММ-ДД` у заголовка колонки (как поле даты отчёта) с кнопкой «Подтвердить дату», если дата есть и у колонки `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` | перепечатывать ту же дату ради подтверждения неочевидно; формат ввода — как у существующего поля даты | нет | +| P6-12 | Панель файлов: у строки документа — значок с числом «предупреждения файла (`job.warnings`) + `issue_counts.error` + `issue_counts.info`» документа, красный при хотя бы одном `error` (в файле или `issue_counts.error > 0`); у строки файла без документов — только `job.warnings`; нуль — значка нет. Клик открывает список: предупреждения файла (`message` и `locator`), `job.mapping_warnings` (как `info`) и `DocumentOut.issues` документа, загружаемые при открытии (`GET /documents/{id}`) | `DocumentBrief.issue_counts` (план 3 P3-23) помечает документ без запроса на каждый; подробности — по клику | акцептованное нарушение тоже даёт значок — подробности в списке | +| P6-13 | `job.warnings` на клиенте нормализуется так же, как ядро: строка → `{code: "LEGACY_WARNING", severity: "info", message, locator: null}`, объект с `code`, `severity ∈ {error, info}`, `message` — как есть, прочее пропускается, не список → `[]` | экран не падает при ошибке порядка выкладки | нет | +| P6-14 | Сценарий T11 — на локальном стенде; строки `unreadable` / `missing` / `detail` и записи `issues` всех видов подставляются перехватом `GET` и `PATCH …/statement` (`page.route`) из фикстуры плана, пока сервисы шлют 1.x; на реальных формах 1.x — регрессия | до шага 5 выкладки 2.0-данных на стенде нет | повторный прогон на `aep_3` после шага 5 — follow-up | +| P6-15 | Цвета: ячейка `needs_review` — `#fff7e6`, исправлено — `#e6f4ff`, строка `unreadable` / `missing` целиком — `#fff1f0`, строка `ok` с `needs_review` — фон ячеек наименования и кода `#fff7e6`; подтверждённая ячейка — маленькая серая галочка перед суммой; уровень `error` — красный значок `ExclamationCircleOutlined` `#cf1322`, `info` — синий `InfoCircleOutlined` `#1677ff` | палитра уже используется на экране и в панели файлов | только вид | +| P6-16 | Ячейка кода: `ok` — код; `unreadable` — серым `line_code_text` (пусто — «—»), подсказка «Код не распознан: «<текст>»»; `missing` — серым «—», подсказка «Кода в строке нет»; `detail` — пусто, подсказка «Расшифровка — в суммы формы не входит»; у `ok` в подсказке «Уверенность кода: N%» (если меньше 100 и код не правлен), «Код подтверждён», «Код назначен аналитиком» | спека: «в ячейке кода серым — сырой текст с подсказкой» | только вид | +| P6-17 | `header.source.<поле>.confidence` — `string \| null` (строка Decimal, как прочие уверенности API, план 3 P3-20), в подсказке — `Number()` и округление до целого | одно правило для всех уверенностей | нет | +| P6-18 | `node_modules` worktree — симлинк на установленный каталог с eslint, prettier и vitest 3.1.4; поэтому вместо `make check` (он начинается с `npm install` и поставил бы пакеты в чужой каталог) гоняется `make lint typecheck test build` — те же проверки без установки | память владельца: симлинк, как в прежних срезах | нет; в описании PR сказано, что install пропущен | +| P6-19 | Предупреждения ОСВ-документа (`DocumentOut.issues`, `OSV_*`) — блок `IssuesPanel` над таблицей экрана ОСВ, все записи — у документа (строки ОСВ не адресуются `line_ids`) | спека: «предупреждения листа ОСВ … из `DocumentOut.issues[]`»; экран документа уже загружает `DocumentOut` | нет | + +## Чего не хватает в API ядра для экрана (`O-n`) + +Все пункты закрыты сведением с планом 3 (18.10.2026, его P3-20…P3-24; спека `arm-core` и гайд §5.6.1 дополнены); +маркеры сохранены. + +- **O-1.** Закрыт: `LineOut.line_code_confidence` (строка Decimal) в A8 плана 3; тип — `string | null` (T1). +- **O-2.** Закрыт: у 409 `LINE_CODE_TAKEN` и 422 `INVALID_LINE_CODE`, `UNKNOWN_LINE`, `INVALID_LINE_PATCH`, + `LINE_CODE_REQUIRED` в `detail` есть `line_id` и `line_code` (P3-24); экран ставит ошибку у поля строки (P6-9, + `lineErrorTarget`, T7). +- **O-3.** Закрыт: `StatementOut.edition_lines [{code, name}]` (P3-22), у формы без даты — коды всех редакций; подсказка + кода — из него (P6-1). +- **O-4.** Закрыт: `DocumentBrief.issue_counts {error, info}` (P3-23); панель помечает документ без запроса (P6-12, + `rowBadge`, T10). +- **O-5.** Закрыт: `header.source.<поле>.confidence` — строка Decimal (P3-20); P6-17. +- **O-6.** Закрыт: `StatementHeaderOut.okei` (P3-21); в блоке шапки поле не показывается (четыре поля T5), запись `issues` + с `field = "okei"` по-прежнему уходит в список документа (Review Focus 2). + +## Открытые вопросы владельцу + +- **В-1.** Приёмка экрана на шаге 4 выкладки — по перехвату с фикстурой (P6-14) и регрессии на формах 1.x; проверка на + реальных 2.0-данных (`aep_3`, РБ 1 кв. 2025) — после перехода сервисов (шаг 5), отдельной задачей. Устраивает? +- **В-2.** Снят 18.10.2026: `DocumentBrief.issue_counts` (план 3 P3-23) — панель помечает и документы только с + предупреждениями документа (P6-12). +- **В-3.** Подтверждённая ячейка помечается маленькой галочкой (P6-15) — в спеке и макете вида нет. Устраивает? + +--- + +## Task 0: Подготовка — worktree, issue, ветка, OpenSpec change arm-web + +**Files:** +- Create: `docs/superpowers/plans/2026-10-18-statements-v2.md` (копия этого плана), `openspec/changes/statements-v2/proposal.md`, + `design.md`, `specs/document-review-ui/spec.md`, `specs/primary-files-ui/spec.md` + +**Interfaces:** +- Produces: worktree на ветке `feat/43-statements-v2`; хук require-plan пропускает правки `src/` (план с именем + ветки лежит в `docs/superpowers/plans/`); `design.md` с разделом `## Decisions made without discussion` (туда — `P6-n` и + рулинги контроллера). + +- [ ] **Step 1: Worktree и зеркальный workspace** (порядок — `~/.claude/CLAUDE.md`, «Роли инструментов»): + +```bash +WS=/Users/andrey.epifancev/projects/awp-ca +mkdir -p $WS/.wt/normalization-web +git -C $WS/arm-web fetch origin +git -C $WS/arm-web worktree add -b tmp/statements-v2 $WS/.wt/normalization-web/arm-web origin/test +git -C $WS/docs fetch origin +# до слияния arm-ka-docs#116 — ветка change; после — origin/main +git -C $WS/docs worktree add --detach $WS/.wt/normalization-web/docs origin/feat/116-unified-normalization +for l in fixtures reference-old-arm CLAUDE.md .claude arm-core; do ln -s $WS/$l $WS/.wt/normalization-web/$l; done +``` + + Пути — абсолютные (`git -C worktree add` считает относительный путь от репозитория). +- [ ] **Step 2: node_modules** (P6-18) — симлинк на каталог, где есть eslint, prettier и vitest 3.1.4: + +```bash +for c in $WS/.wt/fmt/arm-web/node_modules $WS/.wt/web-proto/arm-web/node_modules; do + [ -d $c/eslint ] && [ -d $c/prettier ] && grep -q '"version": "3.1.4"' $c/vitest/package.json && echo $c && break +done +``` + + Найденный — `ln -s <путь> $WS/.wt/normalization-web/arm-web/node_modules`. Не нашёлся — `npm install` в worktree + (свой каталог; `package-lock.json` не коммитить) и отметка `O-n` в ledger. Проверка: `cd $WS/.wt/normalization-web/arm-web + && make lint typecheck test build` — PASS на базе `test`. +- [ ] **Step 3: Issue и ветка:** + +```bash +tea issues create --login home --repo RSHB/arm-web \ + --title "Единая нормализация ББ/ОФР: экран формы и карточка файла по API v5.19 (arm-ka-docs#116)" \ + --description "Спека: arm-ka-docs, change unified-statement-normalization, specs/arm-web/spec.md. План 6: openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-6-arm-web.md. Выкладка вместе с ядром (plan 3 А+Б): JobBrief.warnings меняет тип." +git -C $WS/.wt/normalization-web/arm-web branch -m feat/43-statements-v2 +cd $WS/.wt/normalization-web/arm-web && codegraph init +``` + + Все вызовы `codegraph_explore` — с `projectPath=$WS/.wt/normalization-web/arm-web`; пустой ответ — один раз отметить в + ledger и дальше Read/Grep. +- [ ] **Step 4: Копия плана** — этот файл копируется в `docs/superpowers/plans/2026-10-18-statements-v2.md` с подстановкой + номера issue вместо `43` (`sed "s/43//g"`); проверка: `grep -c "feat/-statements-v2"` ≥ 1. +- [ ] **Step 5: Change OpenSpec** — `openspec new change statements-v2`; `proposal.md`: ссылка на change docs + (`unified-statement-normalization`, arm-ka-docs#116), что меняется (по разделу Goal), non-goals (переключатель «как в + файле», склейка форм), зависимость от ядра v5.19. Спеки-дельты (формулировки требований — из `specs/arm-web/spec.md` + docs, дословно): + - `document-review-ui`: **MODIFIED** «Форма ББ/ОФР — просмотр и правка» (подсветка «требует внимания» — по + `needs_review`; правка расширена строками, колонками, подтверждениями, наименованием организации; полный текст + требования с изменёнными фразами); **MODIFIED** «Подтверждение документа» (порядок 409 с `LINES_UNRESOLVED`, + `DUPLICATE_FORM`, отключение кнопки); **ADDED** «Раскладка строк формы со статусами», «Подсветка и предупреждения у + места», «Правка строк, колонок и подтверждение в режиме «Редактировать»», «Предупреждения ОСВ-документа» (P6-19) — + со сценариями из docs; + - `primary-files-ui`: **ADDED** «Предупреждения файла и документа в карточке файла». + `design.md` — раздел «Decisions made without discussion» с таблицей `P6-1…P6-19` (дата 2026-10-18). +- [ ] **Step 6:** `openspec validate statements-v2 --strict` — без ошибок. +- [ ] **Step 7: Commit** `openspec(statements-v2): change и план экрана формы по API v5.19 (arm-ka-docs#116, #43)`. + +## Task 1: Типы API v5.19, фикстура, строки сетки + +**Files:** +- Modify: `src/types/api.ts`, `src/widgets/DocumentReview/statementGrid.ts`, `src/utils/labels.ts`, + `src/widgets/DocumentReview/statementGrid.test.ts`, `src/widgets/DocumentReview/formLayout.test.ts` (хелпер `row()`), + `src/widgets/FilesDrawer/documentRows.test.ts` (`warnings: []` — тип) +- Create: `src/widgets/DocumentReview/__fixtures__/statement-v2.json`, `src/utils/labels.test.ts` (дополнить, если есть) + +**Interfaces:** +- Produces (в `src/types/api.ts`): + +```ts +export type LineStatus = 'ok' | 'unreadable' | 'missing' | 'detail'; +export type Severity = 'error' | 'info'; +export type IssueKind = 'warning' | 'relation' | 'soft'; +export type PeriodKind = 'absolute' | 'relative:current' | 'relative:prev_year_end' | 'relative:prev_prev_year_end' + | 'relative:prev_period' | 'none'; +export interface FileWarningOut { code: string; severity: Severity; message: string; locator: string | null } +export interface IssueOut { + id: string; kind: IssueKind; code: string; severity: Severity; message: string; + line_ids: string[]; column_id: string | null; column_index: number | null; field: string | null; + source: 'service' | 'core'; relation: string | null; codes: string[]; expected: string | null; actual: string | null; +} +export interface HeaderSourceOut { text: string | null; confidence: string | null } +export interface EditionLineOut { code: string; name: string } +export interface IssueCountsOut { error: number; info: number } +export interface LineErrorDetail { code: string; message: string; line_id: string; line_code: string | null } +export interface UnresolvedLineOut { line_id: string; row_index: number | null; line_code_text: string | null } +export type CellTarget = { line_id: string } | { line_code: string }; +export type CellPatch = (CellTarget & { column_id: string; amount: string | null }) + | { line_id: string; column_id: string; confirmed: boolean }; +export type LinePatch = { line_id: string; line_code: string } | { line_id: string; line_status: 'detail' } + | { line_id: string; confirmed: boolean }; +export interface ColumnPatch { column_id: string; period_date: string } +export interface StatementPatch { + header?: { inn?: string; report_date?: string; units?: string; organization_name?: string }; + values?: CellPatch[]; columns?: ColumnPatch[]; lines?: LinePatch[]; +} +``` + + Изменения существующих: `JobBrief.warnings: FileWarningOut[]` (`mapping_warnings` — `string[]`); `DocumentOut.issues: + IssueOut[]` (`warnings: string[]` остаётся); `StatementHeaderOut` + `needs_review_fields: string[]`, `source: + Partial>`, `organization_name_modified: boolean`, `okei: string | null`; `ColumnOut` + + `period_kind: PeriodKind`, + `label_confidence: string | null`; `LineOut` + `row_index: number | null`, `source_ref: string | null`, `line_status: + LineStatus`, `line_code_text: string | null`, `line_code_confidence: string | null`, `reference_name: string | null`, + `parent_row_index: number | null`, `needs_review: boolean`, `is_confirmed: boolean`, `line_code_modified: boolean`; + `ValueOut` + `text: string | null`, `needs_review: boolean`, `is_confirmed: boolean`; `FailedCheckOut` + `codes: + string[]`, `line_ids: string[]`; `StatementOut.document.form_variant: 'full' | 'simplified' | null`; + `StatementOut.issues: IssueOut[]`, `StatementOut.edition_lines: EditionLineOut[]`; `DocumentBrief.issue_counts: + IssueCountsOut` (и `DocumentOut`, который его расширяет). +- Produces (`statementGrid.ts`): + +```ts +export type GridRow = { + key: string; lineId: string | null; code: string | null; + name: string | null; // line_name — из файла + referenceName: string | null; // справочное имя по коду + origin: 'source' | 'template'; parentCode: string | null; + lineStatus: LineStatus; rowIndex: number | null; parentRowIndex: number | null; + codeText: string | null; codeConfidence: string | null; + needsReview: boolean; isConfirmed: boolean; codeModified: boolean; + cells: Record; +}; +export function buildStatementGrid(statement: StatementOut): { columns: ColumnOut[]; rows: GridRow[] }; +export function cellFlags(value: ValueOut | undefined): { attention: boolean; modified: boolean; confirmed: boolean }; +``` + + Ключ строки — прежний: `id`, у достроенной — `code-<код>`. `draftToPatch`, `applyDraftInput`, `emptySublineKeys`, + `cellKey` пока остаются (переезд — T6). +- Produces (`labels.ts`): `lineStatusLabel(s: LineStatus): string` — `ok` «Строка формы», `unreadable` «Код не + распознан», `missing` «Кода нет», `detail` «Расшифровка»; `severityLabel(s: Severity): string` — `error` «Ошибка», + `info` «Информация». +- Produces (фикстура `statement-v2.json`, тип `StatementOut`; у **каждой** пары «строка × колонка» есть `ValueOut`): + документ `BALANCE`, `NEEDS_CHECK`, 2025 `Q3`, `reporting_date "2025-09-30"`, `form_variant "full"`, `edition "fsbu4"`; + шапка: ИНН `7700000000` (уверенность `100`), дата отчёта `2025-09-30` (`original_value "30 сентября 2025 г."`, + уверенность `67`), единицы «тыс. руб.», `organization_name "ООО Ромашка"`, `needs_review_fields ["report_date"]`, + `okei "384"`, `source {organization_name: {text: "ООО «Ромашкa»", confidence: "67.00"}, report_date: {text: "30 + сентября 2025 г.", confidence: "67.00"}}`; колонки `c1` (`column_index 2`, «На 30 сентября 2025 г.», `2025-09-30`, `absolute`) и `c2` + (`column_index 3`, `label null`, `period_date null`, `none`); строки: + +| id | `row_index` | статус | код / `line_code_text` | наименование (`reference_name`) | `parent_row_index` | особенности (колонка `c1`) | +|---|---|---|---|---|---|---| +| `l-1110` | 0 | ok | 1110 | Нематериальные активы (то же) | — | `10` | +| `l-1150` | 1 | ok | 1150 | Основные средства (то же) | — | `70045` | +| `l-d2` | 2 | detail | — | в том числе: пригодные к использованию | 1 | `70045`, `text "70045"`, уверенность `67`, `needs_review false` | +| `l-1100` | 3 | ok | 1100 | Итого по разделу I (то же) | — | `70055` | +| `l-1210` | 4 | ok | 1210 | Запасы (то же) | — | `100` | +| `l-1230` | 5 | ok | 1230, уверенность кода `50`, строка `needs_review true` | «Дебиторская задолжен-ность» (`Дебиторская задолженность`) | — | `428311`, `text "42831 1"`, `is_recognized false`, `needs_review false` | +| `l-1240` | 6 | ok | 1240 | Финансовые вложения (то же) | — | `5` | +| `l-u7` | 7 | unreadable | `ИЧ,250` | Денежные средства | — | `50`, уверенность `67`, `needs_review true` | +| `l-m8` | 8 | missing | `null` | Прочие оборотные активы | — | `7` | +| `l-1200` | 9 | ok | 1200 | Итого по разделу II (то же) | — | `200`, `needs_review true` | +| `l-1600` | 10 | ok | 1600 | БАЛАНС (то же) | — | `70255` | +| `l-1370a` | 11 | ok | 1370 | Нераспределенная прибыль (то же) | — | `500` | +| `l-1370b` | 12 | ok | 1370 | в т.ч. прошлых лет (Нераспределенная прибыль) | — | `300` | +| `l-d13` | 13 | detail | — | справочно | `null` | `amount null`, `string_value "211(1 |"` | +| `null` | `null` | ok, `origin template` | 1250 | (Денежные средства и денежные эквиваленты) | — | все пустые, `id` значений `null` | +| `l-t1170` | `null` | ok, `origin template` | 1170 | (Финансовые вложения) | — | `3`, `is_modified true` (строка создана правкой) | + + `issues`: `i1` warning `ROW_SPLIT` info, `line_ids [l-1370a, l-1370b]`; `i2` warning `COLUMN_WITHOUT_DATE` error, + `column_id c2`; `i3` warning `ORGANIZATION_UNCERTAIN` info, `field "organization_name"`; `i4` warning `COLUMN_SKIPPED` + info, `column_index 5`, `column_id null`; `i5` warning `REPORT_DATE_UNCERTAIN` error, `field "report_date"`; `i6` + relation `RELATION_FAILED` error, `line_ids [l-1200, l-1210, l-1230, l-1240]`, `column_id c1`, `relation "1200 = 1210 + 1220 + + 1230 + 1240 + 1250 + 1260"`, `codes [...]`, `expected "155"`, `actual "200"`; `i7` warning `NAME_MISMATCH` info, source + `core`, `line_ids [l-1230]`. `checks.failed` — одно нарушение с теми же `codes`, `line_ids`, `column_index 2`. + `edition_lines` — коды ББ `fsbu4` по одному в порядке бланка с первыми именами (в том числе `{code: "1250", name: + "Денежные средства и денежные эквиваленты"}`, `{code: "1370", name: "Нераспределенная прибыль (непокрытый убыток)"}`, + `{code: "1170", name: "Финансовые вложения"}`; кода `1120` нет). + +- [ ] **Step 1: Write the failing tests** (`statementGrid.test.ts`): + - `buildStatementGrid переносит поля v2` — на фикстуре строка `l-u7`: `lineStatus 'unreadable'`, `rowIndex 7`, + `codeText 'ИЧ,250'`, `code null`, `key 'l-u7'`; `l-d2`: `parentRowIndex 1`; шаблонная 1250: `key 'code-1250'`, + `rowIndex null`, `referenceName 'Денежные средства и денежные эквиваленты'`; `l-1230`: `needsReview true`, + `codeConfidence '50'`. + - `cellFlags — внимание только по needs_review` — `{confidence: '50', is_recognized: false, needs_review: false}` → + `attention false`; `{confidence: '100', needs_review: true}` → `attention true`; `{is_confirmed: true}` → `confirmed + true`; `undefined` → все `false`. + - `labels.test.ts`: четыре подписи статуса и две уровня — дословно, как в Interfaces. +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/statementGrid.test.ts src/utils/labels.test.ts` — FAIL. +- [ ] **Step 3: Implement** типы, фикстуру, `GridRow`, `cellFlags`, подписи; хелперы существующих тестов (`row()` в + `formLayout.test.ts`, `v()` в `statementGrid.test.ts`, `job` в `documentRows.test.ts`) дополняются полями по умолчанию + (`lineStatus 'ok'`, `rowIndex` по порядку, `needs_review false` и т. д.), ожидания старых тестов не меняются, кроме + теста `cellFlags` по уверенности — он заменяется новым. +- [ ] **Step 4:** `make lint typecheck test build` — PASS (компоненты, читающие `job.warnings` как строки, на `test` нет). +- [ ] **Step 5: Commit** `feat(types): API формы v5.19 — статусы строк, needs_review, issues, предупреждения файла (#43)`. + +## Task 2: Раскладка — вложения строк без кода и расшифровок, фильтр показа, имена + +**Files:** +- Modify: `src/widgets/DocumentReview/formLayout.ts`, `src/widgets/DocumentReview/formLayout.test.ts` + +**Interfaces:** +- Consumes: `GridRow` (T1). +- Produces: + +```ts +export type LayoutRow = + | { kind: 'section'; key: string; title: string; level: 1 | 2 } + | { kind: 'line'; key: string; row: GridRow; total: boolean; grand: boolean; + child: boolean; // подстрока под родителем или расшифровка — отступ + displayName: string; // справочное имя, иначе файловое без приклеенных заголовков + secondaryName: string | null; // файловое имя, если отличается (P6-4) + fullName: string | null }; // сырое line_name для подсказки, если показано не оно +export function formLayout(type: DocumentType, rows: GridRow[]): LayoutRow[]; +export function filterLayout(layout: LayoutRow[], opts: { showDetails: boolean; hiddenKeys: ReadonlySet }): LayoutRow[]; +export function detailCount(rows: GridRow[]): number; +``` + + `formLayout` раскладывает по разделам только строки `lineStatus === 'ok'` (как сейчас), одинаковые коды — по + `rowIndex` (`null` последним), подстроки под родителем — по коду, затем `rowIndex`; затем вставляет вложения по P6-2. + Расшифровки возвращаются всегда; скрывает их `filterLayout`. + +- [ ] **Step 1: Write the failing tests** (`shape` в тесте: раздел — `# <заголовок>`, строка `ok` — код, `unreadable` / + `missing` — `?`, `detail` — `> `): + - `строка без кода под ближайшей предыдущей по row_index` — `1230 r0, 1240 r1, unreadable 'ИЧ,250' r2, 1250 r3` → + в разделе II: `1230`, `1240`, `?ИЧ,250`, `1250`; + - `якорь по файлу, а не по коду` — `1250 r0, unreadable r1, 1210 r2` → `1210`, `1250`, `?…`; + - `нет якоря — в начало первого раздела` — `missing r0, 1110 r1` (ББ) → `['# АКТИВ', '# I. Внеоборотные активы', '?-', + '1110', …]`; ОФР `missing r0, 2110 r1` → первая строка `?-`; + - `расшифровки под родителем по row_index с отступом` — `1150 r4, d r6 (parent 4), d r5 (parent 4)` → `1150`, `> r5`, + `> r6`, у обеих `child true`; + - `расшифровка без родителя — в начало первого раздела` — `parentRowIndex null` и `parentRowIndex 99` (нет такой + строки); + - `повтор кода: по row_index, вложение у своего экземпляра` — `2410 r9, 2410 r6, unreadable r7` (ОФР) → `2410` (r6), + `?…`, `2410` (r9) — Review Focus 1; + - `шаблонная строка не якорь` — `1250 (rowIndex null, template)`, `unreadable r1` после `1210 r0` → вложение под 1210; + - `якорь — пятизначная подстрока` — `1150 r0, 11501 r1 (parentCode 1150), unreadable r2` → `1150`, `11501`, `?…`; + - `filterLayout скрывает расшифровки и пустые подстроки, вложение скрытого якоря остаётся` — `showDetails false` убирает + `> …`; `hiddenKeys {11501}` убирает `11501`, `?…` остаётся после `1150` — Review Focus 1; + - `detailCount` — фикстура → `2`; форма без `detail` → `0` (Review Focus 4); + - `имена` — `referenceName 'Дебиторская задолженность'`, `name 'Дебиторская задолжен-ность'` → `displayName` справочное, + `secondaryName null`; `name 'Прочие оборотные'` → `secondaryName 'Прочие оборотные'`; `referenceName 'Гудвил'`, + `name 'АКТИВ I. Внеоборотные активы Гудвил'` → `secondaryName null`, `fullName` = сырое имя; строка без + `referenceName` — `displayName` из `name`, `secondaryName null`; + - `фикстура целиком` — порядок: `# АКТИВ`, `# I. Внеоборотные активы`, `> справочно` (l-d13 без родителя), `1110`, + `1150`, `> в том числе…`, `1170`, `1100`, `# II. Оборотные активы`, `1210`, `1230`, `1240`, `?ИЧ,250`, `?-`, `1250`, + `1200`, `1600`, `# ПАССИВ`, `# III. Капитал и резервы`, `1370` (a), `1370` (b). +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/formLayout.test.ts` — FAIL. +- [ ] **Step 3: Implement.** Вставка вложений — отдельной внутренней функцией после построения базовой раскладки: + +```text +anchorKey(a) = detail → ключ строки ok из источника с rowIndex === a.parentRowIndex, иначе null + иначе → ключ строки ok из источника с max rowIndex < a.rowIndex, иначе null (a.rowIndex null → null) +группы по anchorKey, внутри — по rowIndex (null последним), затем key +обход базы: после строки с ключом-якорем — её группа; группа null — после ведущих строк kind = 'section' +``` + + Имена (P6-4): нормализация для сравнения — `toLowerCase`, `ё` → `е`, удалить всё, кроме букв и цифр. +- [ ] **Step 4:** тот же запуск — PASS; `make lint typecheck test build` — PASS. +- [ ] **Step 5: Commit** `feat(statement): раскладка строк без кода и расшифровок на своих местах (#43)`. + +## Task 3: Таблица формы — вид строк и ячеек, переключатель расшифровок + +**Files:** +- Create: `src/widgets/DocumentReview/cellView.ts`, `cellView.test.ts`, `CodeCell.tsx`, `ValueCell.tsx` +- Modify: `src/widgets/DocumentReview/StatementTable.tsx`, `StatementReview.tsx` + +**Interfaces:** +- Consumes: `formLayout`, `filterLayout`, `detailCount` (T2), `cellFlags` (T1). +- Produces: + +```ts +export const COLORS: { attention: '#fff7e6'; modified: '#e6f4ff'; unresolved: '#fff1f0'; section: '#f2f4f7'; grand: '#f9f0ff' }; +export function valueBackground(value: ValueOut | undefined): string | undefined; // modified → attention → нет +export function rowBackground(row: GridRow): string | undefined; // unresolved для unreadable/missing; attention для ok с needsReview (только наименование и код) +export function cellTipLines(value: ValueOut, issues?: IssueOut[]): string[]; +export function codeCellView(row: GridRow): { text: string; muted: boolean; tip: string | undefined }; +``` + + `StatementTable` получает готовые `layout: LayoutRow[]` (раскладку и фильтр считает `StatementReview` в `useMemo`), а не + `rows`. Строка таблицы получает `id="row-"` (`onRow`) — для перехода к строке (T4, T8). + +- [ ] **Step 1: Write the failing tests** (`cellView.test.ts`): + - `cellTipLines` — значение `{text: '42831 1', confidence: '100', is_recognized: false}` → `['Распознано: «42831 1»', + 'Уверенность: 100%']`; исправленное `{is_modified: true, original_amount: '293709'}` → первой строкой `` `Исходное: ${formatAmount('293709', { fraction: 'auto', empty: '-' })}` `` (как сейчас); + `{is_confirmed: true}` → содержит `'Подтверждено аналитиком'`; `text null`, `confidence null` → `[]`; значение + строки `detail` с `needs_review false`, `text '70045'`, `confidence '67'` → `['Распознано: «70045»', 'Уверенность: 67%']` + (подсказка есть и без подсветки, Р-23); + - `codeCellView` — тексты P6-16 дословно для `ok` (с уверенностью 50 → `'Уверенность кода: 50%'`; `codeModified true` → + `'Код назначен аналитиком'`, уверенности нет), `unreadable 'ИЧ,250'` → `{text: 'ИЧ,250', muted: true, tip: 'Код не + распознан: «ИЧ,250»'}`, `missing` → `{text: '—', muted: true, tip: 'Кода в строке нет'}`, `detail` → `{text: '', + muted: true, tip: 'Расшифровка — в суммы формы не входит'}`; + - `valueBackground` / `rowBackground` — `needs_review true` → `#fff7e6`, `is_modified true` → `#e6f4ff` (перекрывает), + `confidence '50'` без `needs_review` → `undefined`; строка `unreadable` → `#fff1f0`, `ok` с `needsReview` → `#fff7e6`. +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/cellView.test.ts` — FAIL. +- [ ] **Step 3: Implement** функции; компоненты: `CodeCell` (просмотр — `codeCellView`; серым — `Typography.Text + type='secondary'`), `ValueCell` (сумма, галочка подтверждения `CheckOutlined` 10px серым, подсказка — строки + `cellTipLines` через `
`); `StatementTable`: колонка наименования — `displayName`, ниже серым `secondaryName`, + подсказка `fullName`; фон строки `unreadable` / `missing` — во всех ячейках строки; `StatementReview`: переключатель + «Показать расшифровки (N)» (`Switch`, как «Показать пустые строки детализации»), показывается при `detailCount > 0`; + пустые подстроки по-прежнему считает `emptySublineKeys` — теперь как `hiddenKeys` для `filterLayout`. +- [ ] **Step 4:** `make lint typecheck test build` — PASS. +- [ ] **Step 5: Сверка с макетом.** `make start`; экран ББ на стенде (форма 1.x) и с перехватом фикстуры (рецепт — T11, + шаг 2): окно 1600×1000, скриншот против `docs/mockup/screens/41-document-check.png` — разделы, итоги, «БАЛАНС» + фиолетовым, строка «ИЧ,250» под 1240 красноватым фоном с серым кодом, переключатель «Показать расшифровки (2)»; + расхождения — в ledger (`O-n`). +- [ ] **Step 6: Commit** `feat(statement): вид строк по статусам, подсветка по needs_review, переключатель расшифровок (#43)`. + +## Task 4: Предупреждения и нарушения у места + +**Files:** +- Create: `src/widgets/DocumentReview/issuePlaces.ts`, `issuePlaces.test.ts`, `IssuesPanel.tsx` +- Modify: `StatementTable.tsx`, `ValueCell.tsx`, `StatementReview.tsx`, `ChecksPanel.tsx` + +**Interfaces:** +- Consumes: `GridRow`, `ColumnOut`, `IssueOut` (T1); `columnTitle` (`reviewView.ts`); `lineStatusLabel`, `severityLabel` (T1). +- Produces: + +```ts +export const HEADER_BLOCK_FIELDS = ['organization_name', 'inn', 'report_date', 'units'] as const; +export interface IssuePlaces { + byRow: Map; byCell: Map; // ключ ячейки — cellKey(rowKey, columnId) + byColumn: Map; byField: Map; document: IssueOut[]; +} +export function issuePlaces(issues: IssueOut[], rows: GridRow[], columns: ColumnOut[], + fields?: readonly string[] /* по умолчанию HEADER_BLOCK_FIELDS */): IssuePlaces; +export interface IssueListItem { id: string; severity: Severity; message: string; place: string; targetKey: string | null } +export function issueListItems(issues: IssueOut[], places: IssuePlaces, rows: GridRow[], columns: ColumnOut[]): IssueListItem[]; +export function worstSeverity(issues: IssueOut[]): Severity | null; +``` + + Правила `issuePlaces` (P6-5): `field` из `fields` → `byField`, `field` вне `fields` → `document`; иначе непустые + `line_ids` → известные строки (по `lineId`); ни одной известной → `document`; есть `column_id` известной колонки → + `byCell` для каждой известной строки, иначе `byRow`; без `line_ids` с известным `column_id` → `byColumn`; всё прочее + (в том числе `column_index` без `column_id`) → `document`. `issueListItems` — только `kind === 'warning'`, сначала + `error`, затем `info`, внутри — по порядку `issues`; `place`: строка `ok` — «строка <код>», `unreadable` / `missing` — + «строка без кода «»» (без текста — «строка без кода»), `detail` — «расшифровка «»»; ячейка — «<строка>, + колонка «»»; колонка — «колонка «»»; поле — «поле «<подпись поля>»»; документ — «документ», + с `column_index` без `column_id` — «столбец вне колонок формы»; `targetKey` — ключ первой строки места + (иначе `null`). + +- [ ] **Step 1: Write the failing tests** (`issuePlaces.test.ts`, на фикстуре): + - `привязка фикстуры` — `i1` в `byRow` у `l-1370a` и `l-1370b`; `i6` в `byCell` у четырёх ячеек `c1`; `i2` в + `byColumn.c2`; `i3`, `i5` в `byField`; `i4` в `document`; `i7` в `byRow.l-1230`; + - `неизвестные строки и поля — у документа` — `line_ids ['l-gone']` → `document`; `line_ids ['l-gone', 'l-1210']` → + только `l-1210`; `field 'okpo'` → `document` — Review Focus 2; + - `issueListItems` — на фикстуре 6 пунктов (без `i6`), первыми `i2` и `i5` (`error`); `i4.place === 'столбец 6 вне + колонок формы'`, `targetKey null`; `i1.place === 'строка 1370'`, `targetKey 'l-1370a'`; запись у `l-d13` → `place + 'расшифровка «справочно»'`; + - `worstSeverity` — `[info, error]` → `'error'`; `[]` → `null`. +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/issuePlaces.test.ts` — FAIL. +- [ ] **Step 3: Implement** функции и UI: значок уровня (P6-15) в ячейке наименования (`byRow`), в углу ячейки значения + (`byCell`, сообщения — в её подсказке после `cellTipLines`), в заголовке колонки (`byColumn`); `IssuesPanel` — `Alert` + «Предупреждения (N)» (тип `error`, если есть `error`, иначе `info`), список с прокруткой (`maxHeight 120`, как + `failureList`), у пункта — тег уровня (`severityLabel`), текст, место и ссылка «Показать» при `targetKey`; + `ChecksPanel` — ссылка «Показать» у нарушения по `line_ids[0]`. Переход к строке — функция `focusRow(key)` в + `StatementReview`: если строка — скрытая расшифровка или скрытая пустая подстрока, сначала включает соответствующий + переключатель; после отрисовки `document.getElementById('row-' + key)?.scrollIntoView({block: 'center'})` и подсветка + рамкой на 2 с (Review Focus 2). +- [ ] **Step 4:** `make lint typecheck test build` — PASS; на перехвате фикстуры: «Показать» у `ROW_SPLIT` прокручивает к + 1370; у записи на `l-d13` при выключенных расшифровках — включает их. +- [ ] **Step 5: Commit** `feat(statement): предупреждения и нарушения у строк, ячеек, колонок и в списке документа (#43)`. + +## Task 5: Шапка формы — наименование организации, поля на проверку, подсказки источника + +**Files:** +- Modify: `src/widgets/DocumentReview/headerFields.ts`, `headerFields.test.ts`, `StatementHeaderBlock.tsx` + +**Interfaces:** +- Consumes: `StatementHeaderOut` (T1), `IssuePlaces.byField` (T4). +- Produces: + +```ts +export type HeaderFieldKey = 'organization_name' | 'inn' | 'report_date' | 'units'; +export type HeaderFieldRow = { field: HeaderFieldKey; label: string; text: string; raw: string | null; + attention: boolean; modified: boolean; tip: string | undefined; issues: IssueOut[]; canConfirm: boolean }; +export function headerFields(header: StatementHeaderOut, byField?: Map): HeaderFieldRow[]; +``` + + Порядок и подписи: «Организация», «ИНН», «Дата отчёта», «Единица измерения». `attention` — поле в + `needs_review_fields` (только); `modified` — `organization_name_modified` или `HeaderFieldOut.modified`; `tip`: + исправлено → «Исходное: » (у организации — «Распознано: «»»), иначе при + наличии `source[field]` — «Распознано: «» · уверенность N%» (`Number(confidence)`, P6-17); `canConfirm` — + `field === 'report_date'` ∧ поле в `needs_review_fields` ∧ `raw !== null`. + +- [ ] **Step 1: Write the failing tests** (`headerFields.test.ts`, фикстура): + - `четыре поля по порядку` — `['organization_name', 'inn', 'report_date', 'units']`, подписи дословно; + - `внимание только по needs_review_fields` — дата отчёта `attention true`, `canConfirm true`; ИНН с уверенностью `50` + вне `needs_review_fields` → `attention false`; пустое ИНН вне списка → `attention false`; + - `подсказка источника` — организация: `tip === 'Распознано: «ООО «Ромашкa»» · уверенность 67%'`; `confidence` строкой + `'67.00'` — тот же текст `67%`; + - `исправленная организация` — `organization_name_modified true` → `modified true`, `tip` с текстом источника; + - `предупреждения поля` — `byField` с `i3` → `issues` у организации — `[i3]`. +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/headerFields.test.ts` — FAIL. +- [ ] **Step 3: Implement**; `StatementHeaderBlock` рисует строку «Организация» шире остальных (ячейка `minWidth 320`), + значки уровня из `issues`, в режиме правки — `Input` у всех четырёх полей и кнопку «Подтвердить дату» при `canConfirm` + (черновик — T6, T8). Блок по-прежнему справа над заголовком формы, как в `41-document-check.png`. +- [ ] **Step 4:** `make lint typecheck test build` — PASS. +- [ ] **Step 5: Commit** `feat(statement): шапка — организация, поля на проверку и подсказки распознанного текста (#43)`. + +## Task 6: Черновик правок и один PATCH + +**Files:** +- Create: `src/widgets/DocumentReview/statementDraft.ts`, `statementDraft.test.ts` +- Modify: `src/widgets/DocumentReview/statementGrid.ts` (удалить `draftToPatch`; `applyDraftInput`, `emptySublineKeys` + остаются), `statementGrid.test.ts` (тесты `draftToPatch` переезжают), `src/utils/dates.ts`, `src/utils/dates.test.ts`, + `StatementReview.tsx`, `StatementTable.tsx`, `StatementHeaderBlock.tsx` + +**Interfaces:** +- Consumes: `GridRow`, `cellKey` (T1), `HeaderFieldKey` (T5), `parseAmountInput` (`utils/money.ts`). +- Produces: + +```ts +// utils/dates.ts +export function isIsoDate(text: string): boolean; // строго ГГГГ-ММ-ДД и существующая дата +// statementDraft.ts +export type LineAction = { kind: 'code'; code: string } | { kind: 'detail' } | { kind: 'confirm'; confirmed: boolean }; +export type HeaderDraft = Partial> & { confirm_report_date?: boolean }; +export interface StatementDraft { + amounts: Record; // cellKey → введённый текст + confirms: Record; // cellKey → confirmed + lines: Record; // lineId → действие + columns: Record; // columnId → введённая дата + header: HeaderDraft; +} +export const emptyDraft = (): StatementDraft => ({ amounts: {}, confirms: {}, lines: {}, columns: {}, header: {} }); +export function isDraftEmpty(draft: StatementDraft): boolean; +export type DraftError = { kind: 'amount' | 'code' | 'date' | 'header' | 'conflict'; key: string; message: string }; +export function buildStatementPatch(draft: StatementDraft, ctx: { rows: GridRow[]; header: StatementHeaderOut }): + { ok: true; body: StatementPatch | null } | { ok: false; errors: DraftError[] }; +``` + + Правила сборки: `values` — суммы (строка с `id` → `line_id`, достроенная → `line_code`, как прежний `draftToPatch`), + затем подтверждения ячеек без суммы в черновике (P6-6); `lines` — действия по P6-7 (код — `^\d{4,5}$`, иначе ошибка + `code` «Код — 4 или 5 цифр»); `columns` — непустые даты, `isIsoDate`, иначе ошибка `date` «Дата в формате ГГГГ-ММ-ДД»; + `header` — `inn`, `report_date`, `units`, `organization_name` после `trim`, непустые (пустое — не отправляется); + `organization_name` длиннее 1000 — ошибка `header`; `confirm_report_date` без введённой даты → `report_date` = текущее + значение; конфликт P6-8 — ошибка `conflict` с `key` строки действия. Нет ни одной части → `body: null`. + +- [ ] **Step 1: Write the failing tests** (`statementDraft.test.ts`, фикстура; `dates.test.ts`): + - `суммы как раньше` — `l-1210:c1 = '120'` → `values [{line_id: 'l-1210', column_id: 'c1', amount: '120'}]`; шаблонная + `code-1250:c1 = '5'` → `{line_code: '1250', …}`; `'12,3,4'` → ошибка `amount`; + - `подтверждение ячейки` — `confirms {l-u7:c1: true}` → `{line_id: 'l-u7', column_id: 'c1', confirmed: true}`; та же + ячейка и в `amounts` → только элемент с `amount` (Review Focus 3); + - `действия строки` — `{l-u7: code '1250'}` → `lines [{line_id: 'l-u7', line_code: '1250'}]`; `{l-m8: detail}` → + `{line_id: 'l-m8', line_status: 'detail'}`; `{l-1230: confirm true}` → `{line_id: 'l-1230', confirmed: true}`; код + `''` → `body null`; код `'1230'` у `l-1230` → `body null`; код `'12a4'` и `'123'` → ошибка `code` с `key 'l-u7'`; + `'11501'` → проходит; + - `действие строки и сумма той же строки` — `{l-m8: detail}` и `l-m8:c1 = '8'` → оба в теле (`lines` и `values`); + - `конфликт шаблонной суммы и кода` — `code-1250:c1 = '5'` и `{l-u7: code '1250'}` → ошибка `conflict`, текст P6-8 + дословно с `1250` — Review Focus 3; + - `колонки` — `{c2: '2024-12-31'}` → `columns [{column_id: 'c2', period_date: '2024-12-31'}]`; `'31.12.2024'` и + `'2024-02-30'` → ошибка `date`; `''` → не отправляется; + - `шапка` — `organization_name ' ООО Ромашка '` → `'ООО Ромашка'`; 1001 символ → ошибка `header`; `confirm_report_date` + → `report_date '2025-09-30'`; `confirm_report_date` и введённая `'2025-06-30'` → `'2025-06-30'`; `inn ''` → нет `inn`; + - `пустой черновик` — `isDraftEmpty(emptyDraft()) === true`, `buildStatementPatch` → `{ok: true, body: null}`; + - `isIsoDate` — `'2024-12-31'` да; `'2024-2-1'`, `'2024-02-30'`, `'31.12.2024'`, `''` нет. +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/statementDraft.test.ts src/utils/dates.test.ts` — FAIL. +- [ ] **Step 3: Implement**; `StatementReview` держит одно состояние `draft: StatementDraft` вместо `draft` и `header`; + `save()` вызывает `buildStatementPatch`: ошибки — `message.error` с первой ошибкой и красная рамка у полей (ключи + ошибок передаются в таблицу и шапку), `body null` — выход из режима правки без запроса, иначе один `patchStatement`; + `dirty` = `!isDraftEmpty(draft)`. Поведение сумм и шапки (ИНН, дата, единицы) не меняется — сценарии W21–W27 + `qa-guide` §8.5 проходят. +- [ ] **Step 4:** `make lint typecheck test build` — PASS. +- [ ] **Step 5: Commit** `refactor(statement): один черновик правок и сборка единственного PATCH (#43)`. + +## Task 7: Режим «Редактировать» — коды строк, меню строки, LINE_CODE_TAKEN + +**Files:** +- Create: `src/widgets/DocumentReview/codeSuggestions.ts`, `codeSuggestions.test.ts`, `lineMenu.ts`, `lineMenu.test.ts`, + `LineMenu.tsx` +- Modify: `CodeCell.tsx`, `StatementTable.tsx`, `StatementReview.tsx` + +**Interfaces:** +- Consumes: `GridRow`, `EditionLineOut`, `LineErrorDetail` (T1), `StatementDraft`, `LineAction` (T6). +- Produces: + +```ts +// codeSuggestions.ts +export type CodeOption = { code: string; name: string }; +export function codeOptions(editionLines: EditionLineOut[], rows: GridRow[]): CodeOption[]; + // P6-1: сначала edition_lines по порядку, затем коды строк ok файла вне справочника (name = referenceName ?? name ?? ''), без повторов +export function filterCodeOptions(options: CodeOption[], query: string, limit?: number /* 20 */): CodeOption[]; +export function lineErrorTarget(error: unknown): { lineId: string; lineCode: string | null } | null; + // P6-9: detail.line_id / detail.line_code ошибки элемента lines[] (409 LINE_CODE_TAKEN, 422 INVALID_LINE_CODE, + // INVALID_LINE_PATCH, UNKNOWN_LINE, LINE_CODE_REQUIRED); иная ошибка или нет line_id — null +// lineMenu.ts +export type LineMenuKey = 'mark_detail' | 'assign_code' | 'change_code' | 'confirm_code' | 'unconfirm_code' | 'reset'; +export function lineMenuItems(row: GridRow, action: LineAction | undefined): { key: LineMenuKey; label: string }[]; +export function applyLineMenu(row: GridRow, key: LineMenuKey, lines: Record): Record; +export function showsCodeInput(row: GridRow, action: LineAction | undefined): boolean; +``` + + Пункты (дословно): `unreadable` / `missing` — «Это расшифровка» (`mark_detail`); `detail` — «Назначить код» + (`assign_code` → действие `{kind: 'code', code: ''}`); `ok` из источника — «Сделать расшифровкой» (`mark_detail`), + «Сменить код» (`change_code` → `{kind: 'code', code: <текущий>}`), «Подтвердить код» (`confirm_code`, при `needsReview` и + не `isConfirmed`), «Снять подтверждение кода» (`unconfirm_code`, при `isConfirmed`); при наличии действия — «Отменить + изменение строки» (`reset`); строка с `lineId === null` — пусто. Поле кода видно у `unreadable` / `missing` всегда и у + любой строки с действием `code`. + +- [ ] **Step 1: Write the failing tests:** + - `codeOptions` (фикстура) — первые элементы — `edition_lines` по порядку; `{code: '1250', name: 'Денежные средства и + денежные эквиваленты'}` и `1370` — по одному разу; код строки `ok` файла вне справочника (добавить в тест строку + `ok` 1231) — в конце, с её именем; у формы с `edition_lines: []` — только коды строк `ok`; нет строк `unreadable` / + `detail`; + - `filterCodeOptions` — `'12'` → коды на `12…`; `'денеж'` → 1250; `'ДЕНЁЖ'` → 1250 (`ё` = `е`, регистр не важен); `''` → + первые 20; + - `lineMenuItems` — `l-u7` → `['mark_detail']`; `l-d2` → `['assign_code']`; `l-1230` (`needsReview`) → `['mark_detail', + 'change_code', 'confirm_code']`; `l-1230` с `isConfirmed` → `['mark_detail', 'change_code', 'unconfirm_code']`; при + действии — последним `reset`; шаблонная 1250 → `[]`; подписи дословно; + - `applyLineMenu` — `confirm_code` → `{kind: 'confirm', confirmed: true}`; `reset` убирает ключ; новое действие заменяет + прежнее; + - `lineErrorTarget` — `ApiError` 409 `{code: 'LINE_CODE_TAKEN', message, line_id: 'l-u7', line_code: '1170'}` → + `{lineId: 'l-u7', lineCode: '1170'}`; 422 `INVALID_LINE_PATCH` с `line_code: null` → `{lineId, lineCode: null}`; 409 + `LINES_UNRESOLVED` → `null`; 422 массивом (pydantic) → `null`. +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/codeSuggestions.test.ts src/widgets/DocumentReview/lineMenu.test.ts` — FAIL. +- [ ] **Step 3: Implement**; `CodeCell` в режиме правки при `showsCodeInput` — `AutoComplete` (ширина колонки кода в + режиме правки — 160) с опциями «<код> — <имя>», значение — код; ошибка формата или строка `lineErrorTarget` ответа — `status='error'` + с подсказкой; у строки с действием `detail` — серый тег «→ расшифровка», с `confirm` — «→ код подтверждён» / + «→ подтверждение снято». `LineMenu` — кнопка `MoreOutlined` в ячейке наименования (`Dropdown`, `trigger click`), + только в режиме правки. Ответ на сохранение с ошибкой элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 коды строк из + Global Constraints): `lineErrorTarget` — красное поле кода этой строки с текстом сервера и `message.error`; строки на + экране нет — тот же текст над таблицей (P6-9). Прочие 422 — `message.error(e.message)`; черновик сохраняется. +- [ ] **Step 4:** `make lint typecheck test build` — PASS; на перехвате фикстуры: ввести 1250 у «ИЧ,250» → перехваченное + тело `PATCH` содержит `lines: [{line_id: 'l-u7', line_code: '1250'}]` и только его. +- [ ] **Step 5: Commit** `feat(statement): правка строк — код с подсказкой из справочника, меню строки, LINE_CODE_TAKEN (#43)`. + +## Task 8: Подтверждение ячеек, даты колонок, правка шапки + +**Files:** +- Create: `src/widgets/DocumentReview/ColumnHeader.tsx`, `src/widgets/DocumentReview/editRules.ts`, `editRules.test.ts` +- Modify: `ValueCell.tsx`, `StatementTable.tsx`, `StatementHeaderBlock.tsx`, `StatementReview.tsx` + +**Interfaces:** +- Consumes: `StatementDraft` (T6), `IssuePlaces.byColumn` (T4), `HeaderFieldRow.canConfirm` (T5). +- Produces: + +```ts +export const COLUMN_DATE_CODES: ReadonlySet; // COLUMN_WITHOUT_DATE, COLUMN_DATE_UNCERTAIN +export function columnDateEditable(col: ColumnOut, columnIssues: IssueOut[]): { editable: boolean; canConfirm: boolean }; +export function cellConfirmState(row: GridRow, value: ValueOut | undefined, draft: StatementDraft, key: string): + 'confirm' | 'unconfirm' | 'pending-confirm' | 'pending-unconfirm' | null; +``` + + `columnDateEditable`: `editable` — (`period_kind === 'none'` ∧ `period_date === null`) или у колонки запись из + `COLUMN_DATE_CODES`; `canConfirm` — `editable` ∧ `period_date !== null` ∧ запись из `COLUMN_DATE_CODES` есть. + `cellConfirmState`: `null`, если у строки или значения нет `id` или в черновике есть сумма этой ячейки; черновик + `confirms[key]` → `pending-*`; иначе `needs_review` → `confirm`, `is_confirmed` → `unconfirm`, прочее — `null`. + +- [ ] **Step 1: Write the failing tests** (`editRules.test.ts`, фикстура): + - `columnDateEditable` — `c2` с `i2` → `{editable: true, canConfirm: false}`; `c1` без записей → `{false, false}`; `c1` с + записью `COLUMN_DATE_UNCERTAIN` → `{true, true}`; + - `cellConfirmState` — `l-u7:c1` (`needs_review`) → `'confirm'`; с `confirms[key] = true` → `'pending-confirm'`; с + суммой в черновике → `null`; ячейка шаблонной 1250 → `null`; значение `is_confirmed true` → `'unconfirm'`. +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/editRules.test.ts` — FAIL. +- [ ] **Step 3: Implement**; `ValueCell` в режиме правки — рядом с полем суммы маленькая кнопка: `confirm` — `CheckOutlined` + «Подтвердить значение», `unconfirm` — `UndoOutlined` «Снять подтверждение», `pending-*` — та же кнопка нажатой + (повторный клик убирает из черновика); `ColumnHeader` — подпись `columnTitle`, значки `byColumn`, в режиме правки при + `editable` — `Input` `ГГГГ-ММ-ДД` (значение — черновик или `period_date`) и «Подтвердить дату» при `canConfirm` (кладёт + в черновик текущую дату); шапка — ввод организации и «Подтвердить дату» (`confirm_report_date`). +- [ ] **Step 4:** `make lint typecheck test build` — PASS; на перехвате: подтвердить ячейку «ИЧ,250», дату `c2` + `2024-12-31`, «Подтвердить дату» отчёта, организацию «ООО Ромашка» — одно сохранение, в теле `values[{…confirmed: + true}]`, `columns`, `header {report_date: '2025-09-30', organization_name: 'ООО Ромашка'}`. +- [ ] **Step 5: Commit** `feat(statement): подтверждение ячеек, дата колонки, наименование организации и подтверждение даты (#43)`. + +## Task 9: «Подтвердить данные» — неразобранные строки и новые 409 + +**Files:** +- Create: `src/widgets/DocumentReview/confirmBlockers.ts`, `confirmBlockers.test.ts` +- Modify: `src/widgets/DocumentReview/confirmError.ts`, `confirmError.test.ts`, `StatementReview.tsx` + +**Interfaces:** +- Consumes: `GridRow` (T1), `focusRow` (T4), `UnresolvedLineOut` (T1). +- Produces: + +```ts +export function unresolvedLines(rows: GridRow[]): GridRow[]; // P6-10, по rowIndex +export const unresolvedHint = (n: number): string => `Разберите строки без кода: ${n}`; +// confirmError.ts +export function confirmErrorInfo(e: unknown): { code: string | undefined; message: string; failures: FailedCheckOut[]; + lines: UnresolvedLineOut[]; documentId: string | null }; +``` + +- [ ] **Step 1: Write the failing tests:** + - `unresolvedLines` (фикстура) — `['l-u7', 'l-m8']`; строка `missing` без значений — не входит; `unreadable` только со + `string_value '211(1 |'` — входит; `detail` с суммой — не входит; форма 1.x (все `ok`) — `[]` (Review Focus 4); + - `unresolvedHint(2) === 'Разберите строки без кода: 2'`; + - `confirmErrorInfo` — `ApiError(409, 'LINES_UNRESOLVED', …, {detail: {lines: [{line_id: 'l-m8', row_index: 8, + line_code_text: null}]}})` → `lines` из одного элемента; `DUPLICATE_FORM` с `detail.document_id 'doc-0'` → + `documentId 'doc-0'`; `CHECKS_NOT_PASSED` — `failures` как раньше; не `ApiError` — прежний текст, `lines []`, + `documentId null`. +- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/confirmBlockers.test.ts src/widgets/DocumentReview/confirmError.test.ts` — FAIL. +- [ ] **Step 3: Implement**; кнопка «Подтвердить данные» отключена при `unresolvedLines(rows).length > 0` (вдобавок к + `reviewActions`), обёрнута в `Tooltip` с `Typography.Link` «Разберите строки без кода: N» → `focusRow(первая.key)`; + ответ 409: `LINES_UNRESOLVED` — красный блок с текстом `unresolvedHint(detail.lines.length)` и ссылкой к первой строке + по `line_id`; `DUPLICATE_FORM` — текст сервера и ссылка «Открыть подтверждённую форму» (`navigate('/documents/' + + documentId)`); `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED` — как сейчас. +- [ ] **Step 4:** `make lint typecheck test build` — PASS; на перехвате: кнопка отключена, подсказка «Разберите строки + без кода: 2», клик ведёт к «ИЧ,250»; перехват `POST …/confirm` с 409 `DUPLICATE_FORM` даёт ссылку. +- [ ] **Step 5: Commit** `feat(statement): блокировка подтверждения при строках без кода, LINES_UNRESOLVED и DUPLICATE_FORM (#43)`. + +## Task 10: Карточка файла и ОСВ — предупреждения структурами + +**Files:** +- Create: `src/widgets/FilesDrawer/fileWarnings.ts`, `fileWarnings.test.ts`, `FileWarningsPopover.tsx` +- Modify: `src/widgets/FilesDrawer/documentRows.ts`, `documentRows.test.ts`, `FilesList.tsx`, + `src/widgets/DocumentReview/OsvDocument.tsx` + +**Interfaces:** +- Consumes: `FileWarningOut`, `IssueOut`, `JobBrief` (T1); `IssuesPanel` (T4); `getDocument` (`services/api/documents.ts`). +- Produces: + +```ts +export function toFileWarnings(value: unknown): FileWarningOut[]; // P6-13 +export function warningsLevel(list: readonly { severity: Severity }[]): Severity | null; +export const warningText = (w: FileWarningOut): string => (w.locator ? `${w.message} — ${w.locator}` : w.message); +// documentRows.ts: DocumentRow + fileWarnings: FileWarningOut[]; mappingWarnings: string[]; issueCounts: IssueCountsOut +export function rowBadge(row: DocumentRow): { count: number; level: Severity } | null; + // P6-12: count = fileWarnings.length + issueCounts.error + issueCounts.info; level 'error', если есть error в файле + // или issueCounts.error > 0; count 0 — null +``` + + `IssuesPanel` получает режим `mode: 'statement' | 'document'`: в `document` все записи — пункты без ссылок (P6-19). + +- [ ] **Step 1: Write the failing tests:** + - `toFileWarnings` — `['sheet=ОСВ 60 строки без родителя']` → `[{code: 'LEGACY_WARNING', severity: 'info', message: + 'sheet=ОСВ 60 строки без родителя', locator: null}]`; `['a', {code: 'JSON_FAILED', severity: 'error', message: 'm', + locator: 'file:x_ofr.json'}, 5, {code: 'X'}, {code: 'Y', severity: 'warn', message: 'z'}]` → два элемента + (`LEGACY_WARNING` «a», `JSON_FAILED`); `'строка'` → `[]`; `null` → `[]`; объект без `locator` → `locator null` — + Review Focus 5; + - `warningsLevel` — `[info, error]` → `'error'`; `[]` → `null`; + - `warningText` — `'m — file:x_ofr.json'`; без `locator` — `'m'`; + - `buildDocumentRows` — файл с двумя документами и `job.warnings` из одного `JSON_FAILED` → у обеих строк + `fileWarnings` длины 1; файл `ERROR` без документов — у строки файла тоже; `job null` → `[]`; `mapping_warnings` + переносятся как есть; `issue_counts` документа → `issueCounts` (строка файла без документов — `{error: 0, info: 0}`); + - `rowBadge` — документ без предупреждений файла с `issue_counts {error: 0, info: 2}` → `{count: 2, level: 'info'}`; + `JSON_FAILED` в файле и `{error: 0, info: 1}` → `{count: 2, level: 'error'}`; `{error: 1, info: 0}` без файла → + `level 'error'`; всё по нулям → `null`. +- [ ] **Step 2:** `npx vitest run src/widgets/FilesDrawer/fileWarnings.test.ts src/widgets/FilesDrawer/documentRows.test.ts` — FAIL. +- [ ] **Step 3: Implement**; `FilesList` — в колонке «Статус» под плашкой значок с числом (`rowBadge` → цвет P6-15), + клик (`stopPropagation`, строка-ссылка не срабатывает) открывает `Popover` `FileWarningsPopover`: «Предупреждения + файла» (тег уровня + `warningText`), «Предупреждения обработки» (`mappingWarnings`, `info`), «Предупреждения документа» + — при открытии `getDocument(documentId)` → `issues` (загрузка — `Spin`, ошибка — «Не удалось загрузить», без + `documentId` раздела нет). `OsvDocument` — `IssuesPanel mode='document'` над таблицей при непустом `document.issues`. +- [ ] **Step 4:** `make lint typecheck test build` — PASS. +- [ ] **Step 5: Сверка с макетом** — панель файлов против `docs/mockup/screens/39-org-documents.png` (1600×1000): значок + не ломает высоту строки и ширину колонки «Статус»; расхождения — в ledger. +- [ ] **Step 6: Commit** `feat(files): предупреждения файла структурами и предупреждения документа в панели файлов и у ОСВ (#43)`. + +## Task 11: Сценарий на стенде, сверка с макетом, документация репозитория + +**Files:** +- Modify: `README.md` (раздел про экран проверки формы — что нового, как включить перехват для демо не нужно), + `CLAUDE.md` arm-web («Архитектура»: новые чистые модули `DocumentReview` — `formLayout`, `issuePlaces`, + `statementDraft`, `cellView`, `codeSuggestions`, `lineMenu`, `editRules`, `confirmBlockers`; `FilesDrawer/fileWarnings`) + +**Interfaces:** +- Consumes: всё выше; стенд — `docs/dev/qa-guide.md` §4.1–§4.4 (ядро — из worktree или `main` arm-core с частью А, лучше + А + Б; `make start` в worktree arm-web), формы — §6.9 (Excel Межениновской) и §8.5 (`aep_6.pdf`, при поднятых + сервисах §6.8). + +- [ ] **Step 1: Регрессия на формах 1.x** (реальный стенд, пользователь `ka_go`): прогнать W19–W28b `qa-guide` §8.5 и + форму Excel `1_Форма1_бух_09.xlsx` (§6.9). Ожидание: раскладка и итоги как прежде; оранжевые только ячейки с + `needs_review` (у чистых — нет); переключателя расшифровок нет; «Подтвердить данные» доступна; в шапке — строка + «Организация»; в панели файлов у заданий, сохранённых до v5.19, предупреждения видны как «Информация». Скриншоты против + `41-document-check.png`, `41-02-document-check-edit.png`, `39-org-documents.png`. +- [ ] **Step 2: Перехват v2** (P6-14) — через `browser_run_code_unsafe` Playwright: + +```js +const fixture = /* содержимое src/widgets/DocumentReview/__fixtures__/statement-v2.json, id документа заменён на открытый */; +await page.route('**/api/v1/documents/*/statement', async (route) => { + if (route.request().method() === 'PATCH') { globalThis.lastPatch = route.request().postDataJSON(); } + await route.fulfill({ json: fixture }); +}); +``` + + Сценарии спеки `arm-web` по очереди: «Код-мусор на своём месте» (под 1240, подсвечена, серый «ИЧ,250» с подсказкой); + «Расшифровки скрыты» (не видно, «Показать расшифровки (2)», включённый показывает под 1150 и в начале раздела I); + «Догадка подтверждена проверкой» (1230: не подсвечена, подсказка «Распознано: «42831 1»»); предупреждения у места + (1370 — значок `ROW_SPLIT`, колонка 2 — `COLUMN_WITHOUT_DATE`, организация и дата — значки, `COLUMN_SKIPPED` — в списке); + «Назначить код» (тело `PATCH` — `lines: [{line_id: 'l-u7', line_code: '1250'}]`); «Подтверждение заблокировано» + (подсказка «Разберите строки без кода: 2», клик ведёт к первой); подтверждение ячейки, дата колонки, организация, + подтверждение даты — одно тело. Перехват `POST …/confirm` с 409 `LINES_UNRESOLVED` и `DUPLICATE_FORM`, `PATCH` с 409 + `LINE_CODE_TAKEN` (`detail {line_id: 'l-u7', line_code: '1170'}`) — тексты и места по T7, T9. Перехват + `GET /legal-entities/*/files` с `job.warnings` (`JSON_FAILED` `error`, `locator 'file:x_ofr.json'`) и + `documents[].issue_counts` — сценарий «Потерянный ОФР в карточке», значок у документа без предупреждений файла. +- [ ] **Step 3:** итог сверки — в ledger и в описание PR: что сверено (экраны 41, 41-02, 39), что отличается и почему + (новые элементы, которых нет в макете: поле кода, меню строки, кнопки подтверждения, значки уровней). +- [ ] **Step 4:** README и CLAUDE.md arm-web; `npm run format`; `make lint typecheck test build` — PASS. +- [ ] **Step 5: Commit** `docs(arm-web): экран формы v5.19 — README и карта модулей (#43)`. + +## Завершение + +- [ ] `make lint typecheck test build` — PASS, вывод — в описание PR (с пометкой P6-18: `npm install` пропущен, node_modules — + симлинк). +- [ ] Финальное ревью ветки (opus): код-ревью TS/React с объяснением решений простым языком для владельца (правило + `arm-web/CLAUDE.md`); таблица находок `## Findings` в ledger (`| Маркер | Суть | Статус |`). +- [ ] PR: `tea pulls create --login home --repo RSHB/arm-web --base test --head feat/43-statements-v2`, ссылка на issue и + на arm-ka-docs#116; в описании — «Rulings I made», `P6-n`, O-1…O-6, В-1…В-3, итог сверки с макетом, `deferred`-находки, + строка «выкладывается только вместе с ядром v5.19 (план 3 А + Б); после merge — `make -C docs sync-web`». Merge — по + команде владельца. +- [ ] После merge в `test`: `make -C /Users/andrey.epifancev/projects/awp-ca/docs sync-web SYNC_ARGS=--check` (ожидание — + код 10, отстаёт), затем `make -C …/docs sync-web` — issue и PR в arm-core (ревью кода против бэкенда — за агентом; merge + — по команде владельца). `arm-core/web` напрямую не править. +- [ ] При «закрывай» — чеклист `~/.claude/CLAUDE.md`: `openspec archive statements-v2` в arm-web, SDD workspace, ветка и + worktree `.wt/normalization-web` (симлинк `node_modules` удаляется вместе с worktree, цель симлинка не трогать), follow-up + issue с `deferred` и повторным прогоном на `aep_3` после шага 5 выкладки (В-1). + +--- + +## Покрытие спеки `arm-web` + +| Требование / сценарий | Задачи | +|---|---| +| Раскладка строк формы со статусами: разделы, `ok` по коду, повторы по `row_index`, шаблонные и пятизначные на местах | T2, T3 | +| … `unreadable` / `missing` под ближайшей предыдущей, без неё — в начале первого раздела; подсветка целиком, серый `line_code_text` | T2, T3 | +| … `detail` под переключателем «Показать расшифровки (N)», под родителем с отступом | T2, T3 | +| … справочное имя основным, файловое рядом при отличии; переключателя «как в файле» нет | T2, T3 | +| Сценарии «Код-мусор на своём месте», «Расшифровки скрыты» | T2 (тесты), T11 | +| Подсветка ячеек по `needs_review` с `text` и уверенностью; строки — по `needs_review` строки | T1, T3 | +| Поля шапки — по `needs_review_fields`, подсказка из `header.source` | T5 | +| `issues` и `checks` у строк, ячеек, полей, документа; `column_index` без `column_id` — у документа; `error` ≠ `info` | T4, T5 | +| Значения `detail` не подсвечиваются, подсказка есть; сценарий «Догадка подтверждена проверкой» | T3 | +| Режим «Редактировать»: поле кода с подсказкой, «Это расшифровка», «Назначить код», «Сделать расшифровкой», смена кода, «Подтвердить код» | T7 | +| Подтверждение и снятие подтверждения ячейки и кода; дата колонки (в том числе подтверждение той же даты) | T7, T8 | +| Наименование организации; подтверждение даты отчёта тем же значением | T5, T8 | +| Один `PATCH` (`header`, `values`, `columns`, `lines`); сценарий «Назначить код» | T6, T7, T11 | +| «Подтвердить данные» отключена с подсказкой «Разберите строки без кода: N» и переходом; 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`, `LINE_CODE_TAKEN` | T7, T9 | +| Сценарий «Подтверждение заблокировано» | T9, T11 | +| Карточка файла: `job.warnings` структурами, `message` и `locator`, `error` ≠ `info`; `DocumentOut.issues`; `mapping_warnings` строками; сценарий «Потерянный ОФР в карточке» | T10, T11 | diff --git a/openspec/changes/statements-v2/.openspec.yaml b/openspec/changes/statements-v2/.openspec.yaml new file mode 100644 index 0000000..5c881ba --- /dev/null +++ b/openspec/changes/statements-v2/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-18 diff --git a/openspec/changes/statements-v2/design.md b/openspec/changes/statements-v2/design.md new file mode 100644 index 0000000..749640c --- /dev/null +++ b/openspec/changes/statements-v2/design.md @@ -0,0 +1,90 @@ +## Context + +Реализация плана 6 сквозного change `unified-statement-normalization` +(`../docs/openspec/changes/unified-statement-normalization/`, спека `specs/arm-web/spec.md`). Вся логика экрана — +чистые функции в `.ts` с vitest-тестами, компоненты React только рисуют их результат. Черновик правок аналитика — +одна структура `StatementDraft`, из которой одна функция собирает тело единственного `PATCH`. Подробный план — +`docs/superpowers/plans/2026-10-18-statements-v2.md`. + +## Decisions made without discussion + +Дата: 2026-10-18. Решения плана, принятые без обсуждения с владельцем; если решение неверно, цена ошибки — в +последнем столбце. + +| Маркер | Решение | Обоснование | Цена ошибки | +|---|---|---|---| +| P6-1 | Подсказка кода — `StatementOut.edition_lines` (справочник строк редакции формы, у формы без даты — всех редакций; план 3 P3-22), затем коды строк `ok` файла, которых в справочнике нет (имя — `reference_name` ?? `line_name`); отдельного запроса нет | справочник приходит с ответом формы | нет | +| P6-2 | Якорь строки `unreadable` / `missing` — строка `ok` из источника (`row_index` не `null`) с наибольшим `row_index` меньше своего; якорь `detail` — строка `ok` из источника с `row_index = parent_row_index`; нет якоря (в том числе `detail` с `parent_row_index = null` или указывающим не на `ok`) — в начало первого раздела (после ведущих заголовков разделов; у ОФР — в начало таблицы). Вложение встаёт сразу после строки-якоря; несколько вложений одного якоря — по `row_index` | спека `arm-web` «Раскладка», S-62 («строка `detail` без родителя на экране — в начале раздела»), P3-17 (тот же порядок вложений в ответе ядра) | нет | +| P6-3 | Скрытие (пустые подстроки, расшифровки) применяется **после** раскладки, фильтром | иначе скрытый якорь сдвинул бы вложения к соседу | нет | +| P6-4 | Справочное имя (`reference_name`) — основное; файловое (`line_name`, после срезания приклеенных заголовков разделов) показывается вторым серым текстом, только если отличается после нормализации: нижний регистр, `ё` → `е`, оставлены только буквы и цифры | переносы OCR («задолжен-ность») не должны давать ложного «отличается» | лишнее или пропущенное второе имя — только показ | +| P6-5 | Маркеры «у места» строятся из **всех** записей `issues` (в том числе `relation` / `soft` — у них `line_ids` + `column_id`); список `IssuesPanel` показывает только `kind = warning`; нарушения §7 остаются списком в `ChecksPanel` (из `checks`, с акцептом) и получают ссылку «Показать» по `line_ids[0]`. Запись с `column_id` без `line_ids` (например, `COLUMN_WITHOUT_DATE`) показывается у заголовка колонки; поле шапки, которого нет в блоке шапки, и строки, которых нет в ответе, — у документа | одна запись не дублируется в двух списках; заголовок колонки — естественное место для предупреждения о дате колонки | нет | +| P6-6 | Подтверждение ячейки — в черновике; доступно, только если у строки есть `id` и у значения есть `id` (у достроенных ячеек — нет, ядро ответит `UNKNOWN_CELL`); сумма в черновике той же ячейки отменяет её подтверждение | `PATCH`: в элементе `values` ровно одно из `amount` / `confirmed`; правка суммы и так снимает `needs_review` | нет | +| P6-7 | Одно действие на строку в черновике (`code` / `detail` / `confirm`), новое заменяет прежнее; пустое поле кода и код, равный текущему, — не действие; у достроенных шаблонных строк (`id = null`) действий строки нет | 422 `INVALID_LINE_PATCH` при двух действиях; `lines[]` адресуется только `line_id` | нет | +| P6-8 | Сумма в шаблонную строку по `line_code` X и назначение кода X другой строке в одном сохранении — ошибка черновика «Код X назначается строке, а в шаблонную строку X введены суммы — сохраните по очереди», запрос не уходит | ядро ответит 422 `UNKNOWN_CELL` (P3-10), а сообщение ядра не говорит, что делать | нет | +| P6-9 | Ошибка элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `LINE_CODE_REQUIRED`) показывается у поля кода строки `detail.line_id` (текст сервера в подсказке поля и `message.error`); строки с этим `id` на экране нет — текстом над таблицей | ядро отдаёт `detail.line_id` и `detail.line_code` (план 3 P3-24) | нет | +| P6-10 | Строка «неразобранная» для кнопки «Подтвердить данные» — `line_status ∈ {unreadable, missing}` и хотя бы у одного значения `amount !== null` или `string_value !== null` (ровно условие ядра для 409 `LINES_UNRESOLVED`) | кнопка и сервер считают одинаково | нет | +| P6-11 | Подтверждение даты отчёта — кнопка «Подтвердить дату» у поля из `needs_review_fields` (уходит `header.report_date` тем же значением); дата колонки — поле `ГГГГ-ММ-ДД` у заголовка колонки (как поле даты отчёта) с кнопкой «Подтвердить дату», если дата есть и у колонки `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` | перепечатывать ту же дату ради подтверждения неочевидно; формат ввода — как у существующего поля даты | нет | +| P6-12 | Панель файлов: у строки документа — значок с числом «предупреждения файла (`job.warnings`) + `issue_counts.error` + `issue_counts.info`» документа, красный при хотя бы одном `error` (в файле или `issue_counts.error > 0`); у строки файла без документов — только `job.warnings`; нуль — значка нет. Клик открывает список: предупреждения файла (`message` и `locator`), `job.mapping_warnings` (как `info`) и `DocumentOut.issues` документа, загружаемые при открытии (`GET /documents/{id}`) | `DocumentBrief.issue_counts` (план 3 P3-23) помечает документ без запроса на каждый; подробности — по клику | акцептованное нарушение тоже даёт значок — подробности в списке | +| P6-13 | `job.warnings` на клиенте нормализуется так же, как ядро: строка → `{code: "LEGACY_WARNING", severity: "info", message, locator: null}`, объект с `code`, `severity ∈ {error, info}`, `message` — как есть, прочее пропускается, не список → `[]` | экран не падает при ошибке порядка выкладки | нет | +| P6-14 | Сценарий T11 — на локальном стенде; строки `unreadable` / `missing` / `detail` и записи `issues` всех видов подставляются перехватом `GET` и `PATCH …/statement` (`page.route`) из фикстуры плана, пока сервисы шлют 1.x; на реальных формах 1.x — регрессия | до шага 5 выкладки 2.0-данных на стенде нет | повторный прогон на `aep_3` после шага 5 — follow-up | +| P6-15 | Цвета: ячейка `needs_review` — `#fff7e6`, исправлено — `#e6f4ff`, строка `unreadable` / `missing` целиком — `#fff1f0`, строка `ok` с `needs_review` — фон ячеек наименования и кода `#fff7e6`; подтверждённая ячейка — маленькая серая галочка перед суммой; уровень `error` — красный значок `ExclamationCircleOutlined` `#cf1322`, `info` — синий `InfoCircleOutlined` `#1677ff` | палитра уже используется на экране и в панели файлов | только вид | +| P6-16 | Ячейка кода: `ok` — код; `unreadable` — серым `line_code_text` (пусто — «—»), подсказка «Код не распознан: «<текст>»»; `missing` — серым «—», подсказка «Кода в строке нет»; `detail` — пусто, подсказка «Расшифровка — в суммы формы не входит»; у `ok` в подсказке «Уверенность кода: N%» (если меньше 100 и код не правлен), «Код подтверждён», «Код назначен аналитиком» | спека: «в ячейке кода серым — сырой текст с подсказкой» | только вид | +| P6-17 | `header.source.<поле>.confidence` — `string \| null` (строка Decimal, как прочие уверенности API, план 3 P3-20), в подсказке — `Number()` и округление до целого | одно правило для всех уверенностей | нет | +| P6-18 | `node_modules` worktree — симлинк на установленный каталог с eslint, prettier и vitest 3.1.4; поэтому вместо `make check` (он начинается с `npm install` и поставил бы пакеты в чужой каталог) гоняется `make lint typecheck test build` — те же проверки без установки | память владельца: симлинк, как в прежних срезах | нет; в описании PR сказано, что install пропущен | +| P6-19 | Предупреждения ОСВ-документа (`DocumentOut.issues`, `OSV_*`) — блок `IssuesPanel` над таблицей экрана ОСВ, все записи — у документа (строки ОСВ не адресуются `line_ids`) | спека: «предупреждения листа ОСВ … из `DocumentOut.issues[]`»; экран документа уже загружает `DocumentOut` | нет | +| R-1 | Шаги «сверка с макетом / прогон в браузере» (T3 s5, T4 s4, T7–T10) перенесены в T11 (дата 2026-10-18) | нет стенда ядра с v5.19, общий Playwright-браузер делят параллельные дорожки | визуальные дефекты найдутся позже, в T11 | +| R-2 | Ссылка «Показать» также у проверок со статусом `soft_failed` (дата 2026-10-18) | согласованное расширение: нарушение так же адресуется строкой | только показ | +| R-3 | Компонент меню строки назван `LineMenuButton.tsx`, а не `LineMenu.tsx` (дата 2026-10-18) | на регистронезависимой ФС (macOS) `LineMenu.tsx` и `lineMenu.ts` дают TS1149 | нет | +| O-1 | Два MODIFIED-требования («Форма ББ/ОФР — просмотр и правка», «Подтверждение документа») составлены исполнителем из текущей спеки: в docs-спеке `arm-web` все требования ADDED. «Предупреждения ОСВ-документа» (P6-19) в docs-спеке нет; написано по плану. Сверить в arm-ka-docs#116 (дата 2026-10-18) | иначе change нельзя было бы описать дельтой | расхождение формулировок со спекой docs при архивации | +| T1.D-1 | `cellFlags` возвращает ещё и `confirmed`; двум старым ожиданиям добавлен `confirmed: false`; тест «внимание по уверенности» заменён на «внимание только по needs_review» (дата 2026-10-18) | `toEqual` падает на новом ключе; так велит бриф | только тесты | +| T1.D-2 | В тестовый хелпер `doc()` добавлен `issue_counts: {error: 0, info: 0}` (дата 2026-10-18) | поле обязательно в `DocumentBrief`, иначе падает tsc | нет | +| T1.D-3 | Значения фикстуры `statement-v2.json`, которых нет в брифе, придуманы исполнителем (форматы сумм, уверенности, `text`, UUID, `codes`, сообщения, `is_current`, пустая колонка c2, порядок `edition_lines`) (дата 2026-10-18) | фикстура должна быть полной | тесты завязаны на выдуманные значения; реальные данные — в T11 | +| T1.D-4 | Фикстура приведена к `StatementOut` через `as` в тесте (дата 2026-10-18) | импорт JSON расширяет литералы до `string`; полноту проверяет рантайм-тест пар «строка × колонка» | слабая проверка типов фикстуры | +| T1.D-5 | Тест «переносит поля v2» проверяет ключи по `l-u7`, `code-1250`, `l-1230`, `l-d2` (дата 2026-10-18) | ровно как в брифе | нет | +| T3.D-1 | Несколько строк подсказки кода склеиваются переводом строки; у `codeModified` уверенность не показывается (дата 2026-10-18) | читаемость подсказки | только вид | +| T3.D-2 | Подсказка сумм принимает необязательный `issues` (добавляет message) (дата 2026-10-18) | задел для T4 | нет | +| T3.D-3 | Уверенность округляется `Math.round(Number())` (P6-17) (дата 2026-10-18) | одно правило для всех уверенностей | нет | +| T3.D-4 | В режиме правки у подтверждённой ячейки галочка — `prefix` поля ввода (дата 2026-10-18) | ячейка в режиме правки — Input | наложение значков, см. T8.D-3 | +| T3.D-5 | `id` строки `row-` выставляется только на строках-линиях, не на заголовках разделов (дата 2026-10-18) | якорь прокрутки нужен только строкам | нет | +| T4.D-1 | Подписи мест полей шапки: organization_name — «Организация», inn — «ИНН», report_date — «Дата отчёта», units — «Единица измерения»; неизвестное поле — своим кодом, запись уходит в `document` (дата 2026-10-18) | P6-5 | только вид | +| T4.D-2 | Запись с `field` из блока шапки всегда в `byField`, независимо от `line_ids` (дата 2026-10-18) | поле шапки — самостоятельное место | нет | +| T4.D-3 | `warning` с `line_ids` без `column_id` — место «строка …», с `column_id` — «<строка>, колонка «…»»; `relation` / `soft` в список не входят (дата 2026-10-18) | P6-5 | только вид | +| T4.D-5 | `onShow` панелей передаёт ключ строки; для достроенных строк (`id = null`) перехода нет — `line_ids` их не содержат (дата 2026-10-18) | нет id — нет адреса | нет ссылки «Показать» у достроенной строки | +| T4.D-6 | Значок у наименования строки — только по `byRow`; записи `byCell` дают значок только в ячейке (дата 2026-10-18) | не дублировать | только вид | +| T5.D-1 | Кнопка «Подтвердить дату» рисуется только при переданном `onConfirmDate` (дата 2026-10-18) | иначе мёртвая кнопка | нет | +| T5.D-2 | У организации в подсказке при modified — «Распознано: «…»»; нет `source` — подсказки нет (дата 2026-10-18) | нечего показывать | нет | +| T5.D-3 | Уверенность в подсказке шапки через `Number()` ('67.00' → 67%) (дата 2026-10-18) | P6-17 | нет | +| T5.D-4 | Placeholder организации — «Наименование организации» (текст не из плана) (дата 2026-10-18) | поле в режиме правки должно быть подписано | только вид | +| T6.D-1 | `buildStatementPatch` для суммы в неизвестную строку возвращает ошибку `amount` (раньше бросалось исключение) (дата 2026-10-18) | экран не должен падать | нет | +| T6.D-2 | Подтверждение ячейки и действие строки у строк без `id` молча пропускаются (дата 2026-10-18) | UI их не создаёт (P6-6, P6-7) | молчаливая потеря действия при ошибке UI | +| T6.D-3 | Код сравнивается после `trim`; пустой или равный текущему — не действие и не ошибка, проверка до формата (дата 2026-10-18) | P6-7 | нет | +| T6.D-4 | Конфликт P6-8 ищется по сумме в шаблонную строку (`id = null`) с тем же кодом; `key` ошибки — `lineId` строки с действием (дата 2026-10-18) | P6-8 | нет | +| T6.D-5 | Тексты ошибок: «Наименование организации — не длиннее 1000 символов» (длина организации); «Исправьте суммы с неверным форматом» (суммы, как раньше) (дата 2026-10-18) | тексты не из плана | только вид | +| T6.D-6 | `confirm_report_date` при отсутствии текущей даты ничего не отправляет (дата 2026-10-18) | нечего подтверждать | пустое тело PATCH, «Сохранить» молчит | +| T6.D-7 | Ошибка `amount` подсвечивает ячейку красной рамкой и сама по себе (дата 2026-10-18) | раньше подсветка шла только по разбору формата | только вид | +| T7.D-2 | Подпись пункта меню: «Это расшифровка» для `unreadable` / `missing`, «Сделать расшифровкой» для `ok` (дата 2026-10-18) | разные смыслы действия | только вид | +| T7.D-3 | Меню строки у `ok` — только при `origin = 'source'`; шаблонные строки с id меню не получают (дата 2026-10-18) | бриф | нет | +| T7.D-4 | `lineErrorTarget` читает `ApiError.body.detail` (отдельного поля `detail` у `ApiError` нет) (дата 2026-10-18) | иначе деталей ошибки не достать | нет | +| T7.D-5 | Ошибка сервера сбрасывается любой правкой черновика (в `setDraft`) (дата 2026-10-18) | иначе устаревшая ошибка висит после правки | нет | +| T7.D-6 | У `unreadable` / `missing` с действием detail / confirm поле кода остаётся, рядом тег (дата 2026-10-18) | бриф: поле всегда | см. T7.2: очистка поля теряет действие | +| T8.D-1 | Подтверждение ячейки без `id` значения молча пропускается в `buildStatementPatch`; кнопка не рисуется (`cellConfirmState → null`) (дата 2026-10-18) | P6-6 | нет | +| T8.D-2 | Ожидающая кнопка даты — disabled с текстом «Дата подтверждена (до сохранения)»; без ожидания «Подтвердить дату»; отменить подтверждение даты можно только отменой всей правки. Для колонки ожидание = черновая дата равна `period_date`. Затем текст подсказки при нажатой кнопке подтверждения ячейки: «Подтверждение будет сохранено — нажмите, чтобы убрать» (дата 2026-10-18) | пользователь должен видеть, что подтверждение в черновике | текст не из плана; ширина колонки (T8.2) проверяется в T11 | +| T8.D-3 | При значке записи у Input `paddingLeft: 14`, чтобы значок не перекрывал галочку (дата 2026-10-18) | без браузера не проверено | визуальное наложение; проверка в T11 | +| T8.D-4 | `cellConfirmState` возвращает `pending-unconfirm` при `confirms[key] === false` (дата 2026-10-18) | нужно третье состояние | нет | +| T8.D-5 | Подтверждение ячейки только строкам с `lineId` (дата 2026-10-18) | P6-6, P6-7 | нет | +| T9.D-1 | «Значения есть» = `amount !== null || string_value !== null` по любой ячейке; `detail` не блокирует независимо от значений (дата 2026-10-18) | P6-10 | расхождение с условием ядра | +| T9.D-2 | Ключ ссылки в 409 `LINES_UNRESOLVED` — `line_id` (дата 2026-10-18) | ключ GridRow для строк из БД | ссылка молчит, если строки нет в таблице (T9.6) | +| T9.D-3 | Описание Alert подтверждения собирается императивно (`confirmDescription`); приоритет: failures > lines > duplicate (дата 2026-10-18) | линтер запрещает вложенные тернарники | только вид | +| T10.D-1 | `toFileWarnings` отбрасывает объекты без `code` / `message` или с `severity` не `error` / `info` (дата 2026-10-18) | P6-13 | нет | +| T10.D-2 | В режиме `document` у `IssuesPanel` колонка «место» не показывается; `OsvDocument` кладёт place '' / targetKey null (дата 2026-10-18) | у документа нет места | нет | +| T10.D-3 | Подробности документа в поповере грузятся при открытии (`trigger=click`), состояние сбрасывается при закрытии (дата 2026-10-18) | P6-12 | лишний запрос при повторном открытии | +| T10.D-4 | Значок — ` + )} + ))} ); -export function ChecksPanel({ checks, editable, dirty, onAccept }: Props) { +export function ChecksPanel({ checks, editable, dirty, onAccept, onShow }: Props) { const [open, setOpen] = useState(false); const [comment, setComment] = useState(''); const [saving, setSaving] = useState(false); @@ -44,14 +57,14 @@ export function ChecksPanel({ checks, editable, dirty, onAccept }: Props) { )} {checks.failed.length > 0 && ( - + )} {checks.soft_failed.length > 0 && ( )} {checks.cross_form && ( diff --git a/src/widgets/DocumentReview/CodeCell.tsx b/src/widgets/DocumentReview/CodeCell.tsx new file mode 100644 index 0000000..fa8bd0a --- /dev/null +++ b/src/widgets/DocumentReview/CodeCell.tsx @@ -0,0 +1,71 @@ +import { AutoComplete, Tag, Tooltip, Typography } from 'antd'; +import { codeCellView } from './cellView'; +import { type CodeOption, filterCodeOptions } from './codeSuggestions'; +import { showsCodeInput } from './lineMenu'; +import type { LineAction } from './statementDraft'; +import type { GridRow } from './statementGrid'; + +/** Правка кода строки: действие черновика, справочник подсказок и ошибка (формат / ответ сервера). */ +export type CodeEditing = { + action: LineAction | undefined; + options: CodeOption[]; + error?: string; + onCodeChange: (code: string) => void; +}; + +const actionTag = (action: LineAction | undefined) => { + if (action?.kind === 'detail') return '→ расшифровка'; + if (action?.kind === 'confirm') return action.confirmed ? '→ код подтверждён' : '→ подтверждение снято'; + return null; +}; + +/** Ячейка кода: просмотр (код, у нераспознанного — серый сырой текст) или, в режиме правки, поле кода с подсказками. */ +export function CodeCell({ row, fontWeight, editing }: { row: GridRow; fontWeight?: number; editing?: CodeEditing }) { + const view = codeCellView(row); + const tag = editing ? actionTag(editing.action) : null; + if (editing && showsCodeInput(row, editing.action)) { + const value = editing.action?.kind === 'code' ? editing.action.code : ''; + return ( + + + ({ value: o.code, label: `${o.code} — ${o.name}` }))} + onChange={editing.onCodeChange} + /> + {tag && ( + + {tag} + + )} + + + ); + } + const content = view.muted ? ( + {view.text} + ) : ( + {view.text} + ); + const withTag = tag ? ( + <> + {content} + + {tag} + + + ) : ( + content + ); + if (!view.tip) return withTag; + return ( + {view.tip}}> + {/* span с минимальной шириной: у расшифровки текст пуст, а подсказка должна открываться */} + {withTag} + + ); +} diff --git a/src/widgets/DocumentReview/ColumnHeader.tsx b/src/widgets/DocumentReview/ColumnHeader.tsx new file mode 100644 index 0000000..b585d98 --- /dev/null +++ b/src/widgets/DocumentReview/ColumnHeader.tsx @@ -0,0 +1,69 @@ +import { Button, Input, Tooltip } from 'antd'; +import type { ColumnOut, IssueOut } from '../../types/api'; +import { SeverityIcon } from './IssuesPanel'; +import { columnDateEditable } from './editRules'; +import { worstSeverity } from './issuePlaces'; +import { columnTitle } from './reviewView'; + +type Props = { + col: ColumnOut; + issues: IssueOut[]; + editing: boolean; + /** введённая, но не сохранённая дата колонки */ + draftDate?: string; + error?: boolean; + onDateChange?: (columnId: string, text: string) => void; + /** «Подтвердить дату»: кладёт в черновик текущую дату колонки */ + onConfirmDate?: (col: ColumnOut) => void; +}; + +/** Заголовок колонки: подпись, значок предупреждения и (в режиме правки) поле даты ГГГГ-ММ-ДД с подтверждением. */ +export function ColumnHeader({ col, issues, editing, draftDate, error = false, onDateChange, onConfirmDate }: Props) { + const worst = worstSeverity(issues); + const title = ( + + {worst && } + {columnTitle(col)} + + ); + const withTip = worst ? ( + ( +
{i.message}
+ ))}> + {title} +
+ ) : ( + title + ); + const { editable, canConfirm } = columnDateEditable(col, issues); + if (!editing || !editable) return withTip; + const pending = canConfirm && draftDate !== undefined && draftDate === col.period_date; + return ( +
+ {withTip} + onDateChange?.(col.id, e.target.value)} + style={{ marginTop: 4, fontWeight: 400 }} + /> + {canConfirm && ( + // короткая подпись, чтобы кнопка помещалась в колонку 112 px и ширина не прыгала (T11.1); длинный текст — в подсказке + + + + )} +
+ ); +} diff --git a/src/widgets/DocumentReview/IssuesPanel.tsx b/src/widgets/DocumentReview/IssuesPanel.tsx new file mode 100644 index 0000000..e679a66 --- /dev/null +++ b/src/widgets/DocumentReview/IssuesPanel.tsx @@ -0,0 +1,58 @@ +import { ExclamationCircleOutlined, InfoCircleOutlined } from '@ant-design/icons'; +import { Alert, Button, Tag } from 'antd'; +import { useState } from 'react'; +import type { Severity } from '../../types/api'; +import { severityLabel } from '../../utils/labels'; +import { type IssueListItem, collapseIssues } from './issuePlaces'; + +const COLLAPSED_LIMIT = 3; // сколько записей видно в свёрнутом списке (T11.11) + +/** Значок уровня (P6-15): error — красный, info — синий. */ +export function SeverityIcon({ severity, style }: { severity: Severity; style?: React.CSSProperties }) { + return severity === 'error' ? ( + + ) : ( + + ); +} + +type Props = { items: IssueListItem[]; mode: 'statement' | 'document'; onShow?: (key: string) => void }; + +/** + * Список предупреждений; пустой — не показывается. Длинный список прокручивается, как список нарушений. + * `statement` — форма ББ/ОФР: место записи и ссылка «Показать»; `document` (ОСВ, P6-19) — простые пункты без места и ссылок. + */ +export function IssuesPanel({ items, mode, onShow }: Props) { + const plain = mode === 'document'; + const [expanded, setExpanded] = useState(false); + if (items.length === 0) return null; + const { shown, hidden } = collapseIssues(items, expanded, COLLAPSED_LIMIT); + return ( + i.severity === 'error') ? 'error' : 'info'} + showIcon + title={`Предупреждения (${items.length})`} + description={ +
+ {shown.map((item) => ( +
+ {severityLabel(item.severity)} + {item.message} + {!plain && ` (${item.place})`} + {!plain && item.targetKey !== null && onShow && ( + + )} +
+ ))} + {(hidden > 0 || expanded) && ( + + )} +
+ } + /> + ); +} diff --git a/src/widgets/DocumentReview/LineMenuButton.tsx b/src/widgets/DocumentReview/LineMenuButton.tsx new file mode 100644 index 0000000..eed3da4 --- /dev/null +++ b/src/widgets/DocumentReview/LineMenuButton.tsx @@ -0,0 +1,24 @@ +import { MoreOutlined } from '@ant-design/icons'; +import { Button, Dropdown } from 'antd'; +import { type LineMenuKey, lineMenuItems } from './lineMenu'; +import type { LineAction } from './statementDraft'; +import type { GridRow } from './statementGrid'; + +/** Меню строки в режиме правки: кнопка «⋮» рядом с наименованием. Пусто — кнопки нет. */ +export function LineMenuButton({ + row, + action, + onSelect, +}: { + row: GridRow; + action: LineAction | undefined; + onSelect: (key: LineMenuKey) => void; +}) { + const items = lineMenuItems(row, action); + if (items.length === 0) return null; + return ( + onSelect(key as LineMenuKey) }}> +