Merge pull request 'Фронт в arm-core/web: CLAUDE.md и README фронта' (#149) from feat/148-claude-md-frontend into main

This commit is contained in:
aep 2026-10-21 09:30:51 +03:00
commit 57f8032dbe
29 changed files with 1398 additions and 20 deletions

View File

@ -178,6 +178,12 @@
- ORM 1:1 с `../docs/model/data-model-v5.19.dbml` (текущая версия; миграции `0001_dbml_v5_13` + `0002_dbml_v5_14` + `0003_dbml_v5_15` + `0004_dbml_v5_16` + `0005_dbml_v5_17` + `0006_dbml_v5_18` + `0007_dbml_v5_19`); `api/scripts/check_dbml_vs_orm.py` (`make dbml-check`, входит в `check-full`) — обязательная проверка.
- Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring.
- Вычисляемые объекты консолидации — `api/src/domain/`, без ORM-таблиц.
- Фронт (`web/`) разрабатывается здесь (с 21.10.2026): фронтовые задачи — worktree arm-core и PR в `main`, как и
бэкенд. Отдельный репозиторий `RSHB/arm-web` заморожен на `test` `de4e477` (= arm-core `3157a15`), синхронизация
`make -C ../docs sync-web` больше не используется; правило «`web/` напрямую не править» отменено. Проверки фронта —
`make web-check` (biome + tsc + build, входит в `check-full`) и из `web/`: `npm test` (vitest), `npm run lint`
(ESLint + Prettier), форматирование — `npm run format`. Спеки экранов — `openspec/specs/*-ui` (перенесены из arm-web@de4e477);
новые и изменённые требования к экранам — в `openspec/changes/` этого репозитория.
- Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.
- `api/arm_statements/` — копия пакета `arm-statements`, напрямую не править: обновление — `../docs/scripts/sync-statements.sh` (`--in-place` в PR фичи), свежесть — `make statements-check` (в `check-full`).
- OpenSpec: `openspec/` здесь; сквозные изменения — change в `../docs`.

View File

@ -360,7 +360,7 @@ curl -s -X POST localhost:8000/api/v1/directories/import -H "Authorization: Bear
## Администрирование и экран /admin
Бэкенд — `/api/v1/admin/*`, все маршруты требуют `USERS_ADMIN` (без права — 403 до любых проверок тела и существования
объектов). Фронтенд — страница `/admin` («Пользователи и роли» в боковом меню; видна только с `USERS_ADMIN`): таблица
объектов). Фронтенд — страница `/admin` («Пользователи и роли» в боковом меню; пункт пока виден всем ролям): таблица
пользователей (создание, смена ролей и активности) и вкладка «Матрица прав» (роль × право). Без права экран показывает
«Нет доступа» и запросов к `/admin/*` не делает (настоящую защиту обеспечивает сервер).
@ -374,7 +374,7 @@ curl -s -X POST localhost:8000/api/v1/directories/import -H "Authorization: Bear
make migrate && make seed-dev # схема, справочники и dev-пользователи
make up # если инфраструктура ещё не поднята
cd api && .venv/bin/python run_server.py # бэкенд, :8000 (в api/.env — DEBUG=true и AUTH_DEV_ISSUER=true)
cd web && npm install && npm run start # фронтенд (rsbuild dev), откроется браузер
cd web && npm install && REACT_APP_ROOT_PATH=/ npm run start # фронтенд (rsbuild dev, :3000), откроется браузер
```
Вход — dev-форма «Вход для разработки» (`web/src/auth/DevLogin.tsx`): введите `admin@armka.local`; форма вызывает
@ -1155,7 +1155,7 @@ api/
Срез 4 — consolidation_periods (выбор источников, §2), consolidated_osv (свод), checklist (чек-лист), policy
(`ensure_version_editable`, §10)
tests/ pytest (юнит и integration/fixtures по маркерам)
web/ фронтенд (React 18 + TypeScript, rsbuild, antd): api/ (клиент), auth/ (вход, профиль), layouts/Admin (/admin); сборка web/dist -> api/web/
web/ фронтенд (React 18 + TypeScript, rsbuild, antd; ведётся здесь с 21.10.2026, см. web/README.md, web/CLAUDE.md): api/ (клиент), auth/, services/api/, store/, layouts/ (страницы), widgets/ (экраны); сборка web/dist -> api/web/
setup/ бутстрап шаблона через Nexus банка (pip.conf, setup.sh)
openspec/ спецификации и изменения (OpenSpec)
docs/superpowers/ планы реализации

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-08

View File

@ -0,0 +1,158 @@
## Context
arm-web выделен из `arm-core/web` командой фронта (3 коммита 06–07.10.2026) и дополнен экранами групп, консолидаций и
загрузки. Обзор 08.10.2026: список консолидаций и карточка версии работают с arm-core; загрузка теряет ошибки (нет
`await`, лишние поля формы); проверка документов (`PDFResultViewer`, `FileViewer`, `FileUploaderModel`) скопирована из
старого АРМ и ходит в `/fsl/...`, `/osv/by-file-id`, `POST /files` мимо клиента API; сводной ОСВ нет; `tsc` — 185 ошибок,
biome — 276.
Демо — 09.10.2026 на локальном стенде; затем прототип уходит в контур на тестирование заказчику и переезжает в
`arm-core/web`, где развивается дальше. Поэтому код держится соглашений шаблона `arm-core/web`.
Источник контракта — `../docs/dev/frontend-api-guide.md` (§5.5–§5.7) и схемы `arm-core/api/src/schemas/`.
## Goals / Non-Goals
**Goals:** аналитик на стенде проходит путь «создание консолидации → загрузка ОСВ и PDF → проверка и подтверждение →
сводная ОСВ» без сбоев и консоли разработчика; `make check` зелёный; все запросы — к API arm-core.
**Non-Goals:** см. proposal.md («Non-goals»).
## Decisions
### D-1. Чинить на месте, новое — по образцу arm-core/web (согласовано 08.10.2026)
Оставляем `api/client.ts`, `auth/*`, список консолидаций, карточку версии, модалку создания (с правками), Redux-слайсы
(типизируем). Новые экраны держат состояние в локальных хуках (`useStatement`, `useDocumentEntries`,
`useConsolidatedOsv`) как `arm-core/web/src/widgets/Admin/hooks.ts`. Альтернативы — генерация типов из OpenAPI (новая
dev-зависимость, доступность в Nexus не проверена) и переписывание с нуля (не успеть) — отклонены владельцем.
### D-2. Типы DTO руками
`src/types/` повторяют схемы arm-core только для используемых эндпоинтов: `Page<T>`, `UploadResult`, `FileOut`,
`DocumentBrief`, `DocumentOut`, `StatementOut` (`HeaderOut`, `ColumnOut`, `LineOut`, `ValueOut`, `ChecksOut`),
`EntryOut`, `ConsolidatedRowOut`, `OsvSummaryOut`, `VersionCard` (исправления: `is_head_entity: boolean`,
`entity_type: 'ЮЛ' | 'ИП' | null`, `osv_accounts: string[]`, `changed_by` — UUID). `Decimal` и даты приходят строками —
в типах `string`.
### D-3. Деньги — строки
Суммы не превращаются в `number`. Чистые функции `src/utils/money.ts`: `formatAmount(s)` — разделители разрядов и два
знака без арифметики над float; `parseAmountInput(s)` — нормализация ввода (пробелы, запятая → точка) и проверка
`^-?\d+(\.\d{1,2})?$`, пустое — `null`. Единицы ББ/ОФР — из `header.units`, ОСВ — рубли.
### D-4. Бинарные ответы — `apiFetchBlob`
`src/api/client.ts` получает `apiFetchBlob(path) → {blob, filename | null}` с той же обработкой токена, 401 и ошибок
(тело ошибки — JSON). Используется для скана (`blob.arrayBuffer()` → pdf.js) и экспорта (сохранение через
`URL.createObjectURL` + `<a download>`). Имя экспорта — из `Content-Disposition` (`filename*=UTF-8''…` или `filename=`),
иначе `exportFilename(group, year, quarter)`.
### D-5. Dev-proxy вместо CORS
`rsbuild.config.ts`: `server.proxy['/api'] → process.env.ARM_CORE_URL ?? 'http://localhost:8000'`; в dev
`REACT_APP_API_URL` пустой. Запросы идут с того же источника: нет CORS и читается `Content-Disposition` (follow-up
docs#58 O-1 на фронте обходится). В контуре SPA отдаёт сам arm-core — тот же источник, код не меняется. Гайд фронта §2
(где `REACT_APP_API_URL=http://localhost:8000`) остаётся верным для варианта без proxy.
### D-6. Просмотр PDF
Из `widgets/PDFResultViewer/IFrameBlock.tsx` сохраняется только постраничный рендер pdf.js (canvas, масштаб, листание);
загрузка — `apiFetchBlob('/files/{id}/content?disposition=inline')` → `arrayBuffer`. Компонент переименовывается в
`widgets/DocumentReview/PdfPages.tsx`. Worker pdf.js подключается из пакета `pdfjs-dist` сборкой (без CDN — в контуре
внешнего интернета нет). `PDFDataViewer.tsx`, `types.ts`, `styles.module.css` старого АРМ удаляются.
### D-7. Экран документа
`layouts/DocumentPage.tsx` (`/documents/:document_id`) → `GET /documents/{id}` → `widgets/DocumentReview/StatementReview`
(BALANCE/INCOME) или `widgets/DocumentReview/OsvDocument` (OSV). Чистые функции: `buildStatementGrid(statement)` —
матрица строк × колонок по `column_id` (не по индексу), `buildEntryTree(entries)` — дерево по `parent_entry_id` с
сохранением `row_number`. Правка ячейки — локальный черновик ячеек и «Сохранить» одним `PATCH` со всеми изменёнными
ячейками; ответ сервера заменяет состояние (сервер пересчитывает проверки и сбрасывает акцепт).
### D-8. Панель файлов
`widgets/FilesDrawer` переписывается: `UploadDocumentsModal` → один запрос со всеми файлами, итог из
`summarizeUpload(result)`; список — `FilesList` с вложенными документами; подписи — `fileStatusLabel`,
`documentTypeLabel`, `periodLabel`, `documentStatusLabel` (чистые функции). Опрос — `useDocumentsPolling` с остановкой,
когда нет `UPLOADED`/`PROCESSING`. Слайс `files` типизируется или заменяется локальным хуком (решает исполнитель по
месту, без смены поведения).
### D-9. Сводная ОСВ
`layouts/ConsolidatedOsvPage.tsx` + `widgets/ConsolidatedOsv/` (`useConsolidatedOsv`, `OsvFilters`, `OsvTable`,
`OsvGaps`). Таблица — antd `Table` с серверной пагинацией (`limit` 500). Фильтры — query-параметры как в API; ЮЛ —
из `VersionCard.members`.
### D-10. Тесты — vitest на чистых функциях
`vitest` (devDependency), `npm test` → `vitest run`, цель `make test` входит в `make check`. Покрытие по TDD: money,
`summarizeUpload`, подписи статусов/типов/периодов, `normalizeData` карточки версии, `exportFilename` и разбор
`Content-Disposition`, `buildStatementGrid`, `buildEntryTree`. Компоненты — ручной демо-сценарий.
## Демо-сценарий (приёмка)
Стенд по `../docs/dev/qa-guide.md` §4 и §6.8 (профиль `services`: tabular-parser, scan-gateway, scan-emulator),
`make seed-dev`, вход `ka_rf@armka.local` (или пользователь из гайда с правами EDIT на ГК «Русский бисквит»).
Данные стенда уже подготовлены: консолидация РБ ГСК 2024 Q3 создана (версия `0437f8be-019c-4dbb-9776-08c306aa38cc`),
`aep_6.pdf` уже загружен Счастливочной; повторы дают штатные 409 `CONSOLIDATION_EXISTS` / `DUPLICATE_FILE`.
1. Кнопка «Новая консолидация» — в боковом меню (действует на всё приложение); ГК «Русский бисквит» выбирается в модалке:
ГСК, период 2024 Q2 или 2025 Q3 (для 2024 Q3 — показать 409 как штатную ошибку) → члены ГСК отмечены → «Создать».
2. Шаги 2–4 — на подготовленной версии `0437f8be-…`. Панель «Организации» → РБ → «Файлы»: ОСВ
`fixtures/osv/rb/2024-Q{1,2,3}/*` и Счастливочной (`fixtures/osv/schastl/2024-Q{1,2,3}/*`) загружены, документы
подтверждены (на новой версии — загрузить и подтвердить на экране ОСВ).
3. ББ/ОФР: открыть уже загруженный ББ Счастливочной (или загрузить `fixtures/ocr/aep_6.pdf` РБ) → скан слева, форма справа,
исправить ячейку; ИНН и дата отчёта не распознаны — ввести в шапке дату `2024-09-30` (ГГГГ-ММ-ДД) и «Сохранить», иначе
409 `PERIOD_REQUIRED`; принять нарушения с комментарием (если есть), подтвердить.
4. «К отчётности» → сводная ОСВ: 1385 строк, 22 документа, 7 пробелов; фильтр Счастливочная + «бисквит» → строка `60.01`
по `qa-guide` §7.2; «Скачать xlsx».
Шаги API (2–4) перед браузерным прогоном проверяются curl-скриптом; браузерный прогон — владелец.
## Risks / Trade-offs
- [Круг «ядро + scan-gateway + эмулятор» на стенде целиком не прогонялся (`qa-guide` §6.8: «сервис проверяется
отдельно»)] → Задача 1 плана — прогон до кода. Если сломан: issue в arm-core/scan-gateway, а форма ББ/ОФР на демо —
из xlsx (`fixtures/statements-xlsx/mezheninovskaya`, `qa-guide` §6.9), скан — заглушка «превью недоступно».
- [Объём к завтра] → порядок задач от критичного к желательному; экспорт и пробелы — последними.
- [Ручные типы расходятся с API] → типы только по используемым полям, сверка со схемами при ревью.
- [Подтверждение 22 ОСВ кликами долго на демо] → в README — скрипт подтверждения из `qa-guide` §7.1; на демо можно
показать подтверждение одного документа, остальные подготовить заранее.
## Migration Plan
Один PR `feat/3-demo-prototype` → `test`. Откат — revert PR. Перенос в `arm-core/web` — отдельная задача после демо.
## Open Questions
- Какой ЮЛ «владеет» `aep_6.pdf` в фикстурах (ИНН в шапке) — влияет только на `counterparty_check` (не блокирует
подтверждение); выясняется в задаче 1.
## Decisions made without discussion
- 08.10.2026 — D-5 dev-proxy `/api` вместо `REACT_APP_API_URL=http://localhost:8000`. Обоснование: снимает CORS и
нечитаемый `Content-Disposition` без правок arm-core. Цена ошибки: низкая — переменная `REACT_APP_API_URL` продолжает
работать, proxy только в dev.
- 08.10.2026 — D-6 worker pdf.js из пакета, без CDN. Обоснование: в контуре нет внешнего интернета. Цена ошибки: низкая.
- 08.10.2026 — D-7 правка ячеек пакетом по «Сохранить», а не по каждой ячейке. Обоснование: меньше запросов, сервер
принимает до 5000 ячеек и пересчитывает проверки один раз. Цена ошибки: низкая — поведение меняется в одном хуке.
- 08.10.2026 — D-8: слайс Redux `files` заменён локальным хуком `useLegalEntityFiles` (список нужен только открытой панели). Цена ошибки: низкая.
- 08.10.2026 — R-1: Task 1 (проверка стенда, без записи в worktree) и Task 2 запущены параллельно. Обоснование: срок демо. Цена ошибки: нулевая.
- 08.10.2026 — R-2: замечание T4.I1 (частичный успех загрузки не обновляет список) отложено в Task 6, где модалка переписывалась; закрыто в Task 6. Цена ошибки: нулевая.
- 08.10.2026 — R-3, R-4: scoped re-review однострочного фикса T5.I1 и фикса T7.I1 (чистая функция с тестами) заменены проверкой контроллера по коду. Цена ошибки: низкая.
- 08.10.2026 — R-5: все находки финального ревью ветки исправлены одной fix-волной (384301e, 0f734e3, 67bc2c8), M-5 (тосты после 401) отложена. Цена ошибки: низкая.
- 08.10.2026 — R-6: `make start` задаёт `REACT_APP_ROOT_PATH=/` (иначе белый экран на dev-сервере, C-1). Цена ошибки: низкая — в контуре переменную задаёт платформа.
- 08.10.2026 — R-7: по замечаниям владельца (H-1, H-2) и сверке всех экранов с PNG макета (`docs/mockup/screens/`) экраны
[03], [30], [34], [39], [40], [41], [42] приведены к макету в той же ветке. Цена ошибки: низкая — только вёрстка.
- 08.10.2026 — R-8: суммы — запятая и разделители разрядов (`12 275,68`); в своде и ОСВ ноль показывается «—», в формах
ББ/ОФР — без копеек и «-» для пустых (как в макете). Цена ошибки: низкая — одна функция `formatAmount`.
- 08.10.2026 — R-9: даты загрузки и создания показываются по Москве (`Europe/Moscow`), а не по UTC-строке. Цена: низкая.
- 08.10.2026 — R-10: [34] «Дата верификации» — «—» (в `VersionBrief` нет поля, до маршрута согласования), «ГСК» — «—»
(П-3). Цена: низкая.
- 08.10.2026 — R-11: [30] вкладки Шахматка/КФО/… показаны неактивными (Срезы 5–6); фильтры свода — в заголовках колонок,
признак контрагента — в фильтре «Контрагенты». Цена: низкая.
- 08.10.2026 — R-12: [41] заголовок колонки формы без распознанной подписи — «На <дата периода>» или «Колонка N»; длинный
список нарушений — с прокруткой (120 px). Цена: низкая.
- 08.10.2026 — R-13 (согласовано с владельцем в чате): форма ББ/ОФР [41] раскладывается «как на бумаге» на фронте —
порядок строк и заголовки разделов восстанавливаются по соотношениям §7 (`formLayout.ts`), итоги XX00 жирные (кроме 2900),
строки вне формы — «Прочие строки»; приклеенные OCR заголовки разделов срезаются только при показе (полное имя — в
подсказке). ИНН — только в рамке кодов; ОКУД — по типу формы, если не распознан; длинное название — 2 строки. Причина:
сервер отдаёт строки по коду и без строк-разделов (§12, OQ-36). Цена ошибки: низкая — только показ, данные не меняются;
при появлении порядка строк в API (issue arm-core) раскладку можно упростить.
- 08.10.2026 — R-14 (по замечанию владельца H-5): шапка формы [41] — как в старом АРМ (`reference-old-arm`
`PDFResultViewer`): компактная таблица ИНН / Дата отчёта / Единица измерения (подсветка уверенности и правки, правка в
режиме «Редактировать»), затем «Бухгалтерский баланс на …». Реквизиты бумажной шапки макета (ОКПО, ОКОПФ, ОКФС, адрес,
название организации) не показываются: распознавание их не отдаёт или отдаёт мусор (arm-scan-gateway#15). Заменяет
шапку из R-13. Цена ошибки: низкая — один компонент `StatementHeaderBlock`.

View File

@ -0,0 +1,57 @@
## Why
В пятницу 09.10.2026 — демо владельцу и бизнесу, затем прототип уходит в контур банка на тестирование заказчику
(и переезжает внутрь `arm-core/web`). Сейчас arm-web не проводит аналитика по пути «консолидация → первичка →
проверка → сводная ОСВ»: загрузка теряет ошибки, проверка документов ходит в эндпоинты старого АРМ, экрана сводной ОСВ
нет, `make check` красный (tsc — 185 ошибок, biome — 276).
## What Changes
- Модалка «Новая консолидация» [42]: предотметка членов ГСК, догрузка всех ЮЛ ГК, ошибки API, переход к новой версии.
- Панель «Файлы ЮЛ» [39]/[40] переписана: загрузка в контексте версии с итогом «принято / отклонено», файлы со
вложенными документами, переход к документу, опрос до окончания распознавания.
- Новый экран документа `/documents/:document_id`: для ББ/ОФР [41]/[41-02] — скан PDF рядом с формой, правка ячеек,
проверки и их акцепт, подтверждение; для ОСВ — дерево строк только для чтения и подтверждение.
- Новый экран «Сводная ОСВ» [30] `/groups/:group_id/versions/:version_id/osv`: таблица, фильтры, пробелы, счётчик
несопоставленных, экспорт xlsx.
- `api/client.ts`: `apiFetchBlob` для бинарных ответов (скан, xlsx); dev-proxy `/api` в `rsbuild.config.ts`.
- **BREAKING (внутреннее):** удалены `layouts/FileViewer.tsx`, `components/FileUploaderModel.tsx`,
`widgets/PDFResultViewer/PDFDataViewer.tsx`, `services/api/mocks.ts`; маршрут `/files/:file_id` заменён на
`/documents/:document_id`.
- `make check` зелёный: biome + tsc + vitest + build; добавлен `vitest` (dev) для чистых функций.
## Capabilities
### New Capabilities
- `consolidation-setup`: создание консолидации ГК за период из модалки и переход к её версии.
- `primary-files-ui`: загрузка первичных файлов ЮЛ в контексте версии и список файлов/документов со статусами.
- `document-review-ui`: проверка и подтверждение документа — форма ББ/ОФР со сканом PDF, ОСВ-документ.
- `consolidated-osv-ui`: сводная ОСВ версии — просмотр, фильтры, пробелы, экспорт.
### Modified Capabilities
(нет — `openspec/specs/` пуст)
## Impact
- Экраны макета: [03], [34], [39], [40], [41], [41-02], [42], [30].
- Эндпоинты arm-core (`/api/v1`), только существующие:
`GET /directories/company-groups`, `GET /directories/company-groups/{id}/related-groups`,
`GET /directories/related-groups/{id}`, `GET /directories/legal-entities`, `POST /consolidations`,
`GET /company-groups/{id}/consolidations`, `GET /consolidations/versions/{id}`,
`POST /legal-entities/{id}/files`, `GET /legal-entities/{id}/files`, `GET /files/{id}/content`,
`GET /documents/{id}`, `GET /documents/{id}/entries`, `POST /documents/{id}/confirm`,
`GET|PATCH /documents/{id}/statement`, `POST /documents/{id}/statement/checks/accept`,
`GET /consolidations/versions/{id}/osv`, `GET …/osv/summary`, `GET …/osv/export`.
- Зависимости: `vitest` (devDependency).
- Код: `src/api/`, `src/services/api/`, `src/types/`, `src/widgets/{NewConsoleModal,FilesDrawer,ConsoleTable,…}`,
новые `src/widgets/DocumentReview/`, `src/widgets/ConsolidatedOsv/`, `src/layouts/`, `src/App.tsx`, `rsbuild.config.ts`,
`Makefile`, `README.md`.
## Non-goals
- Блокировка версии («Взять в работу»), сопоставление контрагентов [32-*], экран чек-листа [37], комментарии,
удаление файлов, смена периметра и ручные переходы статусов.
- Правка строк ОСВ (в API нет), картинки страниц и координаты ячеек в скане (в API нет).
- Генерация типов из OpenAPI; автотесты компонентов.
- Перенос в `arm-core/web` и сборка под контур — отдельной задачей после демо.
- Изменения arm-core / scan-gateway: найденный пробел — отдельная issue в том репозитории.

View File

@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Сводка версии
Экран «Сводная ОСВ» [30] SHALL показывать шапку версии (ГК, период, номер, статус) и сводку
`GET /consolidations/versions/{id}/osv/summary`: строк всего, документов, несопоставленных (`unmapped_total` — счётчик
«Осталось N» без перехода к сопоставлению), автоматически сопоставленных.
#### Scenario: Контрольные цифры РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3 после подтверждения 22 ОСВ из `fixtures/osv/{rb,schastl}/2024-Q{1,2,3}`
- **THEN** «строк» = 1385, «документов» = 22, «несопоставленных» = 1381
### Requirement: Таблица свода и фильтры
Экран SHALL показывать строки `GET /consolidations/versions/{id}/osv` постранично (размер страницы ≤ 2000): ЮЛ, счёт,
контрагент, ИНН контрагента, признак контрагента (ГСК/ГК/Внешний), способ сопоставления, сальдо на начало Дт/Кт,
обороты Дт/Кт, сальдо на конец Дт/Кт (суммы — строками с разделителями разрядов). Фильтры: ЮЛ (`legal_entity_id`),
корень счёта (`account_root`), признак контрагента (`counterparty_type`), только несопоставленные (`unmapped`), поиск по
контрагенту (`q`, ≤ 500 символов). Смена фильтра SHALL перечитывать первую страницу.
#### Scenario: Строка из контрольных цифр
- **WHEN** фильтр ЮЛ «Пекарня Счастливочная» и поиск «бисквит»
- **THEN** строка `60.01` показывает сальдо нач. Кт 4 454 151.93, обороты Дт 128 058 143.65, Кт 125 950 450.77,
сальдо кон. Кт 2 346 459.05, признак «ГСК»
### Requirement: Пробелы
Экран SHALL показывать `gaps[]` сводки списком «ЮЛ — счёт — недостающие кварталы» с пояснением, что строки этих пар в
своде отсутствуют.
#### Scenario: Пробелы РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3
- **THEN** показано 7 пробелов, у каждого недостающие кварталы «Q1, Q2»
### Requirement: Экспорт
Кнопка «Скачать xlsx» SHALL получать `GET /consolidations/versions/{id}/osv/export` с текущими фильтрами через клиент API
с токеном и сохранять файл; имя — из `Content-Disposition`, а если заголовок недоступен —
`Сводная_ОСВ_<ГК>_<год>_<квартал>.xlsx`.
#### Scenario: Экспорт с фильтром
- **WHEN** аналитик скачивает свод с фильтром по ЮЛ
- **THEN** сохраняется xlsx, а ошибка сервера показывается текстом, а не скачивается

View File

@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Состав модалки «Новая консолидация»
Модалка [42] SHALL предлагать выбор ГК (`GET /directories/company-groups`), ГСК этой ГК с `kind = gsk`
(`GET /directories/company-groups/{id}/related-groups`), года и квартала, и показывать **все** незакрытые ЮЛ ГК
(`GET /directories/legal-entities?company_group_id=&closed=false`, страницы дочитываются по `offset`, пока не получены
все `total`). Члены выбранной ГСК (`GET /directories/related-groups/{id}` → `members[]`) SHALL быть отмечены
автоматически; пользователь может снять или добавить отметки.
#### Scenario: Выбор ГСК отмечает её членов
- **WHEN** аналитик выбирает ГК «Русский бисквит» и её ГСК
- **THEN** в списке ЮЛ видны все ЮЛ ГК, а члены ГСК («АО "Русский бисквит"», «ООО "Пекарня Счастливочная"») отмечены
#### Scenario: ЮЛ больше одной страницы
- **WHEN** у ГК больше ЮЛ, чем `limit` одной страницы
- **THEN** модалка показывает все ЮЛ ГК, а не первую страницу
### Requirement: Создание и переход к версии
По «Создать» модалка SHALL отправить `POST /consolidations {company_group_id, related_group_id, year, quarter,
legal_entity_ids}` с отмеченными ЮЛ. При 201 модалка SHALL закрыться, список консолидаций ГК — перечитаться, а созданная
версия (`VersionCard.id`) — открыться в карточке. При ошибке модалка SHALL остаться открытой и показать `message` из
`ApiError` (коды `CONSOLIDATION_EXISTS`, `RELATED_GROUP_MISMATCH`, `ENTITY_NOT_IN_GROUP`, `PERIMETER_TOO_SMALL` и 422
валидации).
#### Scenario: Успешное создание
- **WHEN** аналитик создаёт консолидацию 2024 Q3 с периметром из ГСК
- **THEN** открыта карточка версии №1 со статусом «Инициация» и двумя ЮЛ в панели «Организации»
#### Scenario: Консолидация за период уже есть
- **WHEN** сервер отвечает 409 `CONSOLIDATION_EXISTS`
- **THEN** модалка не закрывается и показывает текст ошибки сервера
### Requirement: Переход к сводной ОСВ из карточки
Кнопка «К отчётности» карточки версии SHALL вести на `/groups/:group_id/versions/:version_id/osv` и быть доступна в
любом статусе версии.
#### Scenario: Переход к своду
- **WHEN** аналитик нажимает «К отчётности» у версии
- **THEN** открывается экран сводной ОСВ этой версии

View File

@ -0,0 +1,64 @@
## ADDED Requirements
### Requirement: Экран документа
Маршрут `/documents/:document_id` SHALL загрузить `GET /documents/{id}` и показать вид по `document_type`: `BALANCE` и
`INCOME` — проверка формы, `OSV` — просмотр ОСВ; для прочих типов — карточку документа без данных. Шапка экрана SHALL
показывать тип, ЮЛ-владельца, период и статус документа, кнопку «Назад».
#### Scenario: Открытие формы баланса
- **WHEN** аналитик открывает документ `BALANCE`
- **THEN** показан экран проверки формы
### Requirement: Скан рядом с формой
Для ББ/ОФР экран SHALL показывать исходный файл документа: PDF — постранично через pdf.js из байт
`GET /files/{file_id}/content?disposition=inline`, полученных клиентом API с токеном (не через `<iframe>` и не прямой
ссылкой); изображение (png/jpg) — тегом `<img>` из blob; xlsx — без превью, с кнопкой «Скачать исходник».
#### Scenario: PDF под CSP sandbox
- **WHEN** сервер отдаёт PDF с `Content-Security-Policy: sandbox`
- **THEN** страницы PDF отрисованы на canvas и листаются
### Requirement: Форма ББ/ОФР — просмотр и правка
Экран SHALL показывать `GET /documents/{id}/statement`: шапку (ИНН, дата отчёта, единицы — значение и исходное при
правке; наименование, ОКУД/ОКПО) и таблицу «строка (код, наименование) × колонки» с суммами из `values[]`. Ячейки с
`confidence < 100` или `is_recognized = false` SHALL подсвечиваться как «требует внимания», с `is_modified = true` — как
«исправлено» с исходным значением `original_amount` в подсказке. Единицы (`header.units`) SHALL выводиться над таблицей.
Пока документ не `CONFIRMED`, ячейка SHALL быть редактируемой: ввод проверяется по формату суммы (до 2 знаков после
точки, знак минус допустим, пустое — `null`), сохранение отправляет `PATCH /documents/{id}/statement {values:
[{line_id, column_id, amount}]}` суммой-строкой, ответ заменяет состояние экрана. Правка шапки (ИНН, дата отчёта,
единицы) SHALL отправляться тем же `PATCH` в `header`; `inn: null` не отправляется.
#### Scenario: Исправление ячейки
- **WHEN** аналитик меняет значение ячейки и сохраняет
- **THEN** ячейка помечена «исправлено», проверки пересчитаны по ответу сервера, акцепт снят
#### Scenario: Неверный формат суммы
- **WHEN** аналитик вводит «12,3,4»
- **THEN** запрос не отправляется, поле показывает ошибку формата
### Requirement: Проверки и их акцепт
Экран SHALL показывать `checks`: жёсткие (`failed`), мягкие (`soft_failed`), межформенную (`cross_form`) и акцепт
(`accepted`: кто, когда, комментарий). При непустом `failed` и `accepted = null` SHALL быть доступно «Принять с
комментарием»: комментарий 1–2000 символов → `POST /documents/{id}/statement/checks/accept {comment}`.
#### Scenario: Акцепт нарушений
- **WHEN** есть жёсткие нарушения и аналитик принимает их с комментарием
- **THEN** блок проверок показывает акцепт, нарушения остаются видимыми
### Requirement: Подтверждение документа
Кнопка «Подтвердить» SHALL отправлять `POST /documents/{id}/confirm` и быть недоступна для `CONFIRMED` и `ERROR`. При 409
`CHECKS_NOT_PASSED` экран SHALL показать `detail.failures[]` и предложить акцепт; при 409 `PERIOD_REQUIRED` — подсказать
исправить дату отчёта; прочие ошибки — `message`. После успеха экран SHALL перейти в режим только чтения.
#### Scenario: Подтверждение с неакцептованными нарушениями
- **WHEN** аналитик подтверждает форму с жёсткими нарушениями без акцепта
- **THEN** показан список нарушений из ответа и кнопка «Принять с комментарием»
### Requirement: ОСВ-документ
Для `OSV` экран SHALL показывать строки `GET /documents/{id}/entries` (все страницы, `limit` ≤ 2000) деревом по
`parent_entry_id` с колонками: счёт/субсчёт, контрагент, ИНН, сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец
Дт/Кт (суммы — строками с разделителями разрядов, рубли), только чтение. Кнопка «Подтвердить» — как для форм.
#### Scenario: Подтверждение ОСВ
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`

View File

@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Загрузка файлов ЮЛ в контексте версии
Панель «Файлы ЮЛ» [39]/[40], открытая из строки ЮЛ карточки версии, SHALL загружать выбранные файлы одним запросом
`POST /legal-entities/{id}/files?consolidation_version_id={version_id}` (multipart, поле `files` повторяется), без
других полей формы. Тип и период документа не спрашиваются. После ответа панель SHALL показать итог: принятые файлы
(`accepted[].name`) и отклонённые с причиной (`rejected[].name` + `message`). Ответ 422 с телом `UploadResult` SHALL
трактоваться как «ни один файл не принят» с тем же показом `rejected[]`; ответ 422 с `detail` (`ENTITY_NOT_IN_PERIMETER`)
и прочие ошибки — показом `message`.
#### Scenario: Часть файлов отклонена
- **WHEN** аналитик загружает ОСВ-файл и файл, уже загруженный ранее у этого ЮЛ
- **THEN** первый показан принятым, второй — отклонённым с текстом причины `DUPLICATE_FILE` от сервера
#### Scenario: Все файлы отклонены
- **WHEN** сервер отвечает 422 с телом `UploadResult`
- **THEN** панель показывает каждый отклонённый файл с причиной, а не общую ошибку
### Requirement: Список файлов и документов ЮЛ
Панель SHALL показывать файлы ЮЛ из `GET /legal-entities/{id}/files` (новые первыми): имя, статус файла по-русски
(«Обработка» для `UPLOADED`/`PROCESSING`, «Ошибка» с причиной из `job` для `ERROR`) и вложенные `documents[]` — тип
(«ОСВ <счёт>», «Бух. баланс», «ОФР», «Расшифровка», «Прочее»), период («2024 К1», «2024 · 9 мес.», «период не
определён»), статус документа («Требуется проверка», «Загружен» для `CONFIRMED`, «Ошибка»). Документы `OSV`, `BALANCE`,
`INCOME` SHALL быть ссылками на `/documents/:document_id`.
#### Scenario: PDF с двумя формами
- **WHEN** файл `aep_6.pdf` распознан
- **THEN** под файлом видны два документа — «Бух. баланс» и «ОФР» со статусом «Требуется проверка», каждый открывает
экран проверки
### Requirement: Опрос до окончания распознавания
Пока панель открыта и среди файлов есть `UPLOADED` или `PROCESSING`, панель SHALL перечитывать список раз в 5 секунд и
SHALL прекратить опрос, когда таких файлов нет или панель закрыта. При закрытии панели карточка версии SHALL
перечитываться (обновить ячейки чек-листа).
#### Scenario: Опрос останавливается
- **WHEN** все файлы ЮЛ перешли в `PROCESSED` или `ERROR`
- **THEN** запросы списка файлов прекращаются

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-09

View File

@ -0,0 +1,20 @@
## Context
Решения D-6 и R-7 взяты из design.md arm-core (change `statement-template-lines`, RSHB/arm-core#80).
### D-6. Фронт (arm-web, PR в `test`)
`statementGrid`: ключ строки — `line.id ?? "code-" + line_code` (R-10), ключ ячейки — `rowKey:column_id`; `draftToPatch`
отправляет `line_id` или `line_code` по ключу. Достроенная пустая ячейка (`id = null`) без подсветки «требует проверки»
(`is_recognized = null`), введённое значение подсвечивается как правка. `formLayout` уже раскладывает строки по разделам —
меняется только то, что разделы теперь всегда полные. **Строки «БАЛАНС» (1600 по активу, 1700 по пассиву) выделяются
цветом** (просьба владельца 09.10.2026): фон всей строки — светло-фиолетовый (`#f9f0ff`, purple-1 палитры antd): серый
`#f2f4f7` уже у заголовков разделов, светло-синий `#e6f4ff` — «исправлено», оранжевый `#fff7e6` — «требует проверки»;
шрифт 700 у наименования, кода и сумм. Подсветка ячейки «требует проверки» / «правлено» перекрывает фон строки — сигнал о
данных важнее оформления. Признак — `formLayout` (`LayoutRow.grand: true` для 1600/1700 у `BALANCE`), цвет — `StatementTable`.
## Decisions made without discussion
| № | Дата | Решение | Обоснование | Что стоит ошибка |
|---|---|---|---|---|
| R-7 | 09.10.2026 | Цвет строк «БАЛАНС» — светло-фиолетовый `#f9f0ff` + жирный 700; подсветка ячейки перекрывает фон строки; ОФР не выделяется | Владелец просил выделить баланс по активу и пассиву, цвет не задал; в макете [41] строки «БАЛАНС» не видно; фиолетовый не занят: серый — разделы, синий — «исправлено», оранжевый — «требует проверки» | Цвет не понравится — правка одной константы |
| R-10 | 09.10.2026 | Ключ достроенной строки `code-<code>`, а не `code:<code>` | ':' разделяет rowKey и columnId в cellKey | Нет |

View File

@ -0,0 +1,18 @@
## Why
RSHB/arm-core#80: сервер достраивает форму ББ/ОФР по справочнику редакции — строки и ячейки, которых не дало
распознавание, приходят с `id = null` (`origin = template`). Аналитик должен уметь заполнить их вручную. Кроме того,
итоговые строки «БАЛАНС» (1600 по активу, 1700 по пассиву) сливаются с обычными строками и «Итого по разделу».
## What Changes
- Строки и ячейки с `id = null` отображаются и редактируются как обычные.
- Правка достроенной строки уходит в `PATCH …/statement` по `line_code` (строки из БД — по `line_id`).
- Ключ строки на экране: `id`, а у достроенной — `code-<код>`.
- Строки 1600 и 1700 формы `BALANCE` выделяются фоном `#f9f0ff` и жирным шрифтом.
## Non-goals
- Порядок строк (раскладку по разделам делает `formLayout` как раньше).
- Правка кода и наименования строки.
- Выделение итогов ОФР.

View File

@ -0,0 +1,31 @@
## MODIFIED Requirements
### Requirement: Форма ББ/ОФР — просмотр и правка
Экран SHALL показывать `GET /documents/{id}/statement`: шапку (ИНН, дата отчёта, единицы — значение и исходное при
правке; наименование, ОКУД/ОКПО) и таблицу «строка (код, наименование) × колонки» с суммами из `values[]`, включая
строки и ячейки, достроенные сервером по справочнику редакции (`id = null`). Ячейки с `confidence < 100` или
`is_recognized = false` SHALL подсвечиваться как «требует внимания», с `is_modified = true` — как «исправлено» с
исходным значением `original_amount` в подсказке; пустая достроенная ячейка не подсвечивается. Строки «БАЛАНС» формы
`BALANCE` (коды 1600 и 1700) SHALL выделяться фоном `#f9f0ff` и жирным шрифтом; подсветка ячейки перекрывает фон строки.
Единицы (`header.units`) SHALL выводиться над таблицей. Пока документ не `CONFIRMED`, ячейка SHALL быть редактируемой:
ввод проверяется по формату суммы (до 2 знаков после точки, знак минус допустим, пустое — `null`), сохранение
отправляет `PATCH /documents/{id}/statement {values: [...]}` суммой-строкой: ячейка строки из БД — `{line_id,
column_id, amount}`, ячейка достроенной строки (`id = null`) — `{line_code, column_id, amount}`; ответ заменяет
состояние экрана. Правка шапки (ИНН, дата отчёта, единицы) SHALL отправляться тем же `PATCH` в `header`;
`inn: null` не отправляется.
#### Scenario: Исправление ячейки
- **WHEN** аналитик меняет значение ячейки и сохраняет
- **THEN** ячейка помечена «исправлено», проверки пересчитаны по ответу сервера, акцепт снят
#### Scenario: Заполнение строки, которой не было в распознавании
- **WHEN** у формы нет строки 1700 из источника, аналитик вводит сумму в достроенную строку 1700 и сохраняет
- **THEN** уходит `{line_code: "1700", column_id, amount}`, по ответу строка получила `id`, ячейка помечена «исправлено»
#### Scenario: Строки «БАЛАНС»
- **WHEN** открыта форма `BALANCE`
- **THEN** строки 1600 и 1700 выделены цветом и жирным, у ОФР выделения нет
#### Scenario: Неверный формат суммы
- **WHEN** аналитик вводит «12,3,4»
- **THEN** запрос не отправляется, поле показывает ошибку формата

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-09

View File

@ -0,0 +1,63 @@
## Context
Панель [39] (`widgets/FilesDrawer/FilesDrawer.tsx`) берёт `current_legal_entity_id` и `current_version_id` из стора,
грузит `GET /legal-entities/{id}/files?limit=100` хуком `useLegalEntityFiles` (опрос раз в 5 с, пока есть
`UPLOADED`/`PROCESSING`) и загружает файлы через `uploadFiles(legalEntityId, files, versionId)` с query
`consolidation_version_id`. Список консолидаций ГК (`IConsolidation[]` с `consolidation_id`, `year`, `quarter`,
`versions[] {id, status}`) уже лежит в сторе `consolidations`. Сквозной дизайн — `../docs/openspec/changes/console-files/design.md`.
## Decisions
**D-1. Консолидация и статус версии — селектор, не новое поле стора.** `store/slices/consolidations/selectors.ts`:
чистая функция `findCurrentConsolidation(consolidations, versionId) -> { consolidation_id, year, quarter, version_status }
| null` (ищет версию по `id` в `versions[]`) и хук-обёртка `useCurrentConsolidation()` через `useAppSelector`. Поле
`current_consolidation_id`, упомянутое в сквозном дизайне, не вводится: оно выводится из уже хранимого и не может
рассинхронизироваться с `?version=` (R-1).
**D-2. Один сервис, два маршрута.** `listLegalEntityFiles(legalEntityId, consolidationId: string | null)` и
`uploadFiles(legalEntityId, files, consolidationId: string | null)`: при `consolidationId` путь
`/consolidations/{cid}/legal-entities/{le}/files`, иначе `/legal-entities/{le}/files`. Обработка 422 `UploadResult`
без изменений.
**D-3. Режим панели — локальное состояние `showAll`.** `useLegalEntityFiles(legalEntityId, consolidationId, open)`:
смена `consolidationId` сбрасывает список и перечитывает (как смена ЮЛ). В панели переключатель antd `Switch` с
подписью «Все документы организации» под кнопкой загрузки; `consolidationId = showAll ? null : current.consolidation_id`.
Если консолидация не найдена (версия не из списка) — панель работает в режиме «все» и переключатель скрыт.
**D-4. Подпись консолидации у файла.** `widgets/FilesDrawer/consolidationLabel.ts`:
`consolidationLabel(fileConsolidationId, consolidations, currentConsolidationId) -> string | null`: `null`, если файл в
текущей консоли; «вне консолидации» при `null`; «2025 К1» (`year` + `quarterShortLabel`) по списку; «другая
консолидация», если id нет в списке. `buildDocumentRows(files, labelFor?)` добавляет результат в `subtitle` последним
элементом; `FilesList` получает `labelFor` от панели только в режиме «все».
**D-5. Загрузка только в версии «в работе».** `IN_WORK_STATUSES = ['INITIATED', 'DOCUMENTS_PROCESSING', 'DATA_CORRECTION']`
в `selectors.ts`; кнопка «Загрузить документы» `disabled`, если `version_status` не в списке, с `Tooltip` «Загрузка
доступна только в версии в работе». Модалка получает `consolidationId` вместо `versionId`.
**D-6. Опрос** без изменений: тот же хук, тот же интервал; перечитывается активный список.
## Risks / Trade-offs
- До выката arm-core#96 на стенд панель получит 404 от маршрутов консолидации — ветка вливается после ядра.
- `limit=100` остаётся: стол консоли меньше списка ЮЛ, ограничение стало мягче.
## Decisions made without discussion
1. 09.10.2026 — **R-1:** консолидация и статус версии выводятся селектором из `consolidations[]` + `current_version_id`,
а не хранятся отдельным полем `current_consolidation_id` (сквозной дизайн docs #90 называл поле). Цена ошибки:
низкая — если список ГК в сторе пуст (прямой переход по ссылке до загрузки списка), панель секунду работает в
режиме «все»; список грузится той же страницей.
2. 09.10.2026 — **R-2:** подпись консолидации показывается только в режиме «Все документы организации» и только у файлов
не из текущей консоли; в режиме стола подписи нет (все файлы — этой консоли или подхваченные). Цена ошибки: низкая.
3. 09.10.2026 — **R-3:** неизвестный `consolidation_id` (нет в списке ГК) подписывается «другая консолидация», а не
скрывается. Цена ошибки: нулевая.
4. 10.10.2026 — **R-4:** ручной прогон на стенде отложен до выката arm-core#96. Цена ошибки: UI-дефект всплывёт позже;
PR не вливать до прогона.
5. 10.10.2026 — **R-5:** trailer коммита 5b7b02a «Claude Haiku 5.5» вместо «Claude Fable 5.1» оставлен. Цена ошибки:
неверная атрибуция в одном коммите.
6. 10.10.2026 — **R-6:** промежуточное состояние (tsc-ошибка и versionId в UploadDocumentsModal между Task 2 и Task 3)
принято, чинится в Task 3. Цена ошибки: нулевая, закрыто в Task 3.
7. 10.10.2026 — **R-7:** `consolidation_id` в scripts/demo-prepare.sh берётся из карточки версии
`GET /consolidations/versions/$V`, а не из `POST /consolidations`. Цена ошибки: скрипт демо не запускался на живом ядре.
8. 10.10.2026 — **R-8:** после создания консолидации перечитывается список консолидаций группы (I-1). Цена ошибки:
правка выходит за Impact proposal, лишний запрос.

View File

@ -0,0 +1,51 @@
## Why
Панель «Первичные документы» [39], открытая из строки ЮЛ в карточке версии, показывает все файлы ЮЛ
(`GET /legal-entities/{id}/files`), в том числе загруженные в другие консолидации ГК. Решения владельца 09.10.2026
(RSHB/arm-ka-docs#90, сквозной change `../docs/openspec/changes/console-files/`; ядро — RSHB/arm-core#96): файл
загружается в консолидацию (ГК + период) и образует её «рабочий стол»; ядро даёт маршруты
`POST/GET /consolidations/{cid}/legal-entities/{le}/files`, а query `consolidation_version_id` у
`POST /legal-entities/{id}/files` удаляет. Issue — RSHB/arm-web#26.
## What Changes
Экраны: [39] панель «Первичные документы», [40] модалка «Загрузка документов». Эндпоинты arm-core:
`GET /consolidations/{cid}/legal-entities/{le}/files` (новый), `POST /consolidations/{cid}/legal-entities/{le}/files`
(новый), `GET /legal-entities/{id}/files` (без изменений, в `FileOut` добавлено `consolidation_id`),
`POST /legal-entities/{id}/files` (без query версии).
- Консолидация и статус открытой версии выводятся из стора (`consolidations[]` + `current_version_id`) селектором;
отдельного поля в сторе нет.
- `services/api/files.ts`: `listLegalEntityFiles(legalEntityId, consolidationId)` и
`uploadFiles(legalEntityId, files, consolidationId)` — с консолидацией маршруты `/consolidations/{cid}/…`, без неё
маршруты ЮЛ; query `consolidation_version_id` исчезает.
- Панель [39]: по умолчанию рабочий стол консолидации; переключатель «Все документы организации» переключает на список
ЮЛ; в режиме «все» у файла в подписи — его консолидация («2025 К1» по списку консолидаций ГК, «вне консолидации» при
`null`, «другая консолидация» при неизвестном id). Опрос раз в 5 с — по активному списку.
- Кнопка «Загрузить документы» недоступна, если открытая версия не «в работе» (`INITIATED`, `DOCUMENTS_PROCESSING`,
`DATA_CORRECTION`), с подсказкой.
- Типы: `FileOut.consolidation_id: string | null`.
- Спека `primary-files-ui`: три требования переписаны/добавлены.
## Non-goals
- Экран [60] «Монозаёмщики» и панель без контекста консолидации — не делаются (их пока нет во фронте).
- Пометка «документ не подходит версии» в панели — нет (чек-лист версии, OQ-120).
- Перенос файла между консолидациями, удаление файла из панели — нет (как и сейчас).
- Изменение экрана проверки документа [41] и сводной ОСВ — нет.
- `arm-core/web` (дубль фронта в ядре) не трогается.
## Capabilities
### New Capabilities
### Modified Capabilities
- `primary-files-ui`: загрузка в консолидацию, рабочий стол и переключатель «Все документы организации», недоступная
загрузка вне версии «в работе».
## Impact
- `src/services/api/files.ts` (+ `files.test.ts`), `src/widgets/FilesDrawer/{FilesDrawer,FilesList,UploadDocumentsModal}.tsx`,
`src/widgets/FilesDrawer/{useLegalEntityFiles,documentRows}.ts` (+ тест), новый `src/widgets/FilesDrawer/consolidationLabel.ts`
(+ тест), новый `src/store/slices/consolidations/selectors.ts` (+ тест), `src/types/api.ts`.
- Зависимость: ядро с RSHB/arm-core#96 на стенде; до его выката панель получит 404 на маршрутах консолидации.

View File

@ -0,0 +1,62 @@
## MODIFIED Requirements
### Requirement: Загрузка файлов ЮЛ в контексте версии
Панель «Файлы ЮЛ» [39]/[40], открытая из строки ЮЛ карточки версии, SHALL загружать выбранные файлы одним запросом
`POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (multipart, поле `files` повторяется),
где `consolidation_id` — консолидация открытой версии (из списка консолидаций ГК в сторе по `current_version_id`),
без других полей формы и без query `consolidation_version_id`. Тип и период документа не спрашиваются. Кнопка
«Загрузить документы» SHALL быть недоступна с подсказкой «Загрузка доступна только в версии в работе», если статус
открытой версии не `INITIATED`, `DOCUMENTS_PROCESSING` или `DATA_CORRECTION`. После ответа панель SHALL показать итог:
принятые файлы (`accepted[].name`) и отклонённые с причиной (`rejected[].name` + `message`). Ответ 422 с телом
`UploadResult` SHALL трактоваться как «ни один файл не принят» с тем же показом `rejected[]`; ответ 422 с `detail`
(`ENTITY_NOT_IN_PERIMETER`), 409 (`CONSOLIDATION_NOT_EDITABLE`) и прочие ошибки — показом `message`.
#### Scenario: Часть файлов отклонена
- **WHEN** аналитик загружает ОСВ-файл и файл, уже загруженный ранее у этого ЮЛ
- **THEN** первый показан принятым, второй — отклонённым с текстом причины `DUPLICATE_FILE` от сервера
#### Scenario: Все файлы отклонены
- **WHEN** сервер отвечает 422 с телом `UploadResult`
- **THEN** панель показывает каждый отклонённый файл с причиной, а не общую ошибку
#### Scenario: Загрузка идёт в консолидацию
- **WHEN** панель открыта из версии консолидации 2024 Q3 и аналитик загружает файл
- **THEN** запрос уходит на `/consolidations/{id консолидации 2024 Q3}/legal-entities/{id ЮЛ}/files` без query; после
приёма файл появляется в столе панели
#### Scenario: Версия не в работе
- **WHEN** панель открыта из версии в статусе `CANCELLED`
- **THEN** кнопка «Загрузить документы» недоступна, при наведении — подсказка
### Requirement: Список файлов и документов ЮЛ
Панель SHALL по умолчанию показывать рабочий стол консолидации открытой версии —
`GET /consolidations/{consolidation_id}/legal-entities/{id}/files` (новые первыми): имя, статус файла по-русски
(«Обработка» для `UPLOADED`/`PROCESSING`, «Ошибка» с причиной из `job` для `ERROR`) и вложенные `documents[]` — тип
(«ОСВ <счёт>», «Бух. баланс», «ОФР», «Расшифровка», «Прочее»), период («2024 К1», «2024 · 9 мес.», «период не
определён»), статус документа («Требуется проверка», «Загружен» для `CONFIRMED`, «Ошибка»). Документы `OSV`, `BALANCE`,
`INCOME` SHALL быть ссылками на `/documents/:document_id`. Переключатель «Все документы организации» SHALL переключать
панель на `GET /legal-entities/{id}/files`; в этом режиме у файла не из текущей консолидации в подписи SHALL стоять его
консолидация: «<год> К<n>» по списку консолидаций ГК, «вне консолидации» при `consolidation_id = null`, «другая
консолидация», если id нет в списке. Если консолидация открытой версии не определена (версии нет в списке ГК), панель
SHALL работать в режиме «все» без переключателя.
#### Scenario: PDF с двумя формами
- **WHEN** файл `aep_6.pdf` распознан
- **THEN** под файлом видны два документа — «Бух. баланс» и «ОФР» со статусом «Требуется проверка», каждый открывает
экран проверки
#### Scenario: Стол и все документы
- **WHEN** у ЮЛ есть файлы, загруженные в консолидации 2024 Q3 и 2024 Q4, и файл, загруженный без контекста; панель
открыта из версии 2024 Q3
- **THEN** по умолчанию виден только файл 2024 Q3; после переключателя «Все документы организации» видны все три, у
файла 2024 Q4 подпись «2024 К4», у файла без контекста — «вне консолидации», у файла 2024 Q3 подписи нет
### Requirement: Опрос до окончания распознавания
Пока панель открыта и среди файлов активного списка (стол или все документы) есть `UPLOADED` или `PROCESSING`, панель
SHALL перечитывать этот список раз в 5 секунд и SHALL прекратить опрос, когда таких файлов нет или панель закрыта.
Переключение режима или смена ЮЛ SHALL сбрасывать список и перечитывать его. При закрытии панели карточка версии SHALL
перечитываться (обновить ячейки чек-листа).
#### Scenario: Опрос останавливается
- **WHEN** все файлы активного списка перешли в `PROCESSED` или `ERROR`
- **THEN** запросы списка файлов прекращаются

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-18

View File

@ -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-<key>` выставляется только на строках-линиях, не на заголовках разделов (дата 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 | Значок — `<button>` под плашкой статуса; без `documentId` раздел «Предупреждения документа» не показывается (дата 2026-10-18) | правила a11y | нет |
| T10.D-5 | Пустой список в разделе «Предупреждения документа» — «Нет» (дата 2026-10-18) | текст не из плана | только вид |
| UI-1 | Тексты, которых нет в плане: «Дата подтверждена (до сохранения)», «Подтверждение будет сохранено — нажмите, чтобы убрать», «Строка не найдена в форме», «Наименование организации — не длиннее 1000 символов» (дата 2026-10-18) | нужны для состояний, плана для них не было | только формулировки |
| T11.D-1 | Колонки значений 128px вместо 112 (поле даты из 10 символов и сумма с галочкой обрезались), `tableLayout='fixed'` (дата 2026-10-18) | замечание T11.1: колонка «прыгала» при появлении кнопки даты | только вид |
| T11.D-2 | Кнопка подтверждения даты — короткая «Подтвердить» / «Подтверждено», длинный текст «Подтвердить дату» / «Дата подтверждена (до сохранения)» в подсказке и `aria-label` (дата 2026-10-18) | фиксированная ширина колонки; отклонение от формулировки «Подтвердить дату» в P6-11 | только подпись |
| T11.D-3 | Ссылка «Разберите строки без кода: N» — `Button type=link` отдельной строкой над шапкой карточки, видна и в режиме правки (дата 2026-10-18) | клавиатурный доступ; строки нужно разбирать именно при правке | только вид |
| T11.D-4 | Список предупреждений документа показывает первые 3 записи (ошибки первыми) и «Показать все (N)»; то же на экране ОСВ (дата 2026-10-18) | таблица уезжала ниже сгиба | только вид |
| T11.D-5 | Значение со `string_value` без суммы показывается серым текстом, а не «-» (дата 2026-10-18) | аналитик должен видеть нераспознанный текст | только вид |
Решения контроллера и исполнителей поверх плана (`R-n`, `O-n`) дописываются сюда по ходу исполнения.

View File

@ -0,0 +1,34 @@
## Why
Сквозной change `unified-statement-normalization` (RSHB/arm-ka-docs#116) переводит ББ/ОФР на единую нормализацию:
ядро (API v5.19, контракт 2.0) отдаёт все строки источника со статусами `ok` / `unreadable` / `missing` / `detail`,
признак `needs_review` у ячеек, строк и полей шапки, предупреждения `document_issue` и структурные `job.warnings`.
Экран формы и карточка файла должны это показывать и давать разобрать. Issue — RSHB/arm-web#43. План —
`docs/superpowers/plans/2026-10-18-statements-v2.md` (план 6 сквозного change).
## What Changes
Экраны: [41] проверка формы ББ/ОФР, [39] панель файлов (карточка файла).
- Раскладка «как на бумаге»: строки `unreadable` / `missing` под ближайшей предыдущей строкой с кодом, `detail` под
переключателем «Показать расшифровки (N)», справочное имя строки основное, файловое — рядом, если отличается.
- Подсветка только по `needs_review` (ячейки, строки, поля шапки); предупреждения и нарушения у места
(`issues`, `checks`).
- Режим «Редактировать»: назначение кода, «Это расшифровка», «Сделать расшифровкой», подтверждение ячеек, кодов и дат,
правка наименования организации, один `PATCH …/statement` (`header`, `values`, `columns`, `lines`).
- «Подтвердить данные» отключена при неразобранных строках; разбор 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`,
`LINE_CODE_TAKEN`.
- Карточка файла читает `job.warnings` структурами `{code, severity, message, locator}`; предупреждения ОСВ-документа
и документа — из `DocumentOut.issues`.
- Спеки: `document-review-ui` (2 MODIFIED, 4 ADDED), `primary-files-ui` (1 ADDED).
## Non-goals
- Переключатель «как в файле» (Р-14: раскладка одна).
- Склейка форм с разных листов/файлов.
- Изменения API ядра и спеки docs: расхождения фиксируются как `O-n` в отчётах задач.
## Dependencies
Выкладка вместе с ядром (план 3 сквозного change, API v5.19): `JobBrief.warnings` меняет тип со строк на структуры.
До выкладки 2.0-данных на стенде строки со статусами подставляются перехватом запросов (P6-14).

View File

@ -0,0 +1,109 @@
## MODIFIED Requirements
### Requirement: Форма ББ/ОФР — просмотр и правка
Экран SHALL показывать `GET /documents/{id}/statement`: шапку (ИНН, дата отчёта, единицы — значение и исходное при
правке; наименование, ОКУД/ОКПО) и таблицу «строка (код, наименование) × колонки» с суммами из `values[]`, включая
строки и ячейки, достроенные сервером по справочнику редакции (`id = null`). Ячейки с `needs_review = true` SHALL
подсвечиваться как «требует внимания» (подсветки напрямую по `confidence < 100` или `is_recognized = false` нет), с
`is_modified = true` — как «исправлено» с исходным значением `original_amount` в подсказке; пустая достроенная ячейка не
подсвечивается. Строки «БАЛАНС» формы `BALANCE` (коды 1600 и 1700) SHALL выделяться фоном `#f9f0ff` и жирным шрифтом;
подсветка ячейки перекрывает фон строки. Единицы (`header.units`) SHALL выводиться над таблицей. Пока документ не
`CONFIRMED`, ячейка SHALL быть редактируемой: ввод проверяется по формату суммы (до 2 знаков после точки, знак минус
допустим, пустое — `null`), сохранение отправляет `PATCH /documents/{id}/statement {values: [...]}` суммой-строкой:
ячейка строки из БД — `{line_id, column_id, amount}`, ячейка достроенной строки (`id = null`) — `{line_code,
column_id, amount}`; ответ заменяет состояние экрана. Правка шапки (ИНН, дата отчёта, единицы, наименование
организации) SHALL отправляться тем же `PATCH` в `header`; `inn: null` не отправляется. Правка строк, колонок и
подтверждений в том же `PATCH` (`columns`, `lines`, `values[].confirmed`) описана в требовании «Правка строк, колонок и
подтверждение в режиме «Редактировать»».
#### Scenario: Исправление ячейки
- **WHEN** аналитик меняет значение ячейки и сохраняет
- **THEN** ячейка помечена «исправлено», проверки пересчитаны по ответу сервера, акцепт снят
#### Scenario: Заполнение строки, которой не было в распознавании
- **WHEN** у формы нет строки 1700 из источника, аналитик вводит сумму в достроенную строку 1700 и сохраняет
- **THEN** уходит `{line_code: "1700", column_id, amount}`, по ответу строка получила `id`, ячейка помечена «исправлено»
#### Scenario: Строки «БАЛАНС»
- **WHEN** открыта форма `BALANCE`
- **THEN** строки 1600 и 1700 выделены цветом и жирным, у ОФР выделения нет
#### Scenario: Неверный формат суммы
- **WHEN** аналитик вводит «12,3,4»
- **THEN** запрос не отправляется, поле показывает ошибку формата
### Requirement: Подтверждение документа
Кнопка «Подтвердить данные» SHALL отправлять `POST /documents/{id}/confirm` и быть недоступна для `CONFIRMED` и `ERROR`, а
также при наличии строк `unreadable` / `missing` с суммой или нераспознанным текстом в ячейках (подсказка «Разберите
строки без кода: N» с переходом к первой такой строке). При 409 `CHECKS_NOT_PASSED` экран SHALL показать
`detail.failures[]` и предложить акцепт; при 409 `PERIOD_REQUIRED` — подсказать исправить дату отчёта; при 409
`LINES_UNRESOLVED` — показать тот же текст, что у отключённой кнопки; при 409 `DUPLICATE_FORM` — со ссылкой на
подтверждённую форму; прочие ошибки — `message`. После успеха экран SHALL перейти в режим только чтения.
#### Scenario: Подтверждение с неакцептованными нарушениями
- **WHEN** аналитик подтверждает форму с жёсткими нарушениями без акцепта
- **THEN** показан список нарушений из ответа и кнопка «Принять с комментарием»
#### Scenario: Дубль подтверждённой формы
- **WHEN** сервер отвечает 409 `DUPLICATE_FORM`
- **THEN** экран показывает ошибку со ссылкой на уже подтверждённую форму
## ADDED Requirements
### Requirement: Раскладка строк формы со статусами
Экран проверки формы ББ/ОФР ([41]) SHALL сохранять раскладку «как на бумаге» (Р-14): разделы, внутри — строки `ok` по
возрастанию кода, несколько строк одного кода — в порядке файла (`row_index`), шаблонные строки на своих местах,
пятизначные подстроки под родителем. Строка `unreadable` / `missing` SHALL стоять под ближайшей предыдущей по
`row_index` строкой, размещённой по коду, а без такой — в начале первого раздела; она подсвечена целиком, в ячейке
кода серым — сырой текст `line_code_text` с подсказкой. Строки `detail` SHALL быть скрыты под переключателем
«Показать расшифровки (N)» и показываться под родителем (`parent_row_index`) с отступом. Экран показывает справочное
имя строки (`reference_name`), файловое (`line_name`) — рядом, если отличается. Переключателя «как в файле» нет.
#### Scenario: Код-мусор на своём месте
- **WHEN** строка `unreadable` «ИЧ,250» в файле стоит после строки 1240
- **THEN** на экране она стоит сразу под 1240, подсвечена, в ячейке кода серым «ИЧ,250»
#### Scenario: Расшифровки скрыты
- **WHEN** у формы три строки `detail`
- **THEN** по умолчанию их не видно, переключатель «Показать расшифровки (3)»; включённый показывает их под родителями
### Requirement: Подсветка и предупреждения у места
Экран SHALL подсвечивать ячейки только по `needs_review` (в подсказке — `text` и уверенность), строки — по
`needs_review` строки, поля шапки — по `needs_review_fields` (в подсказке у поля — `header.source.<поле>`: сырой текст и
уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек
(`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` —
столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized =
false` напрямую нет. У значений строк `detail` `needs_review` всегда `false` (Р-23): при показанных расшифровках их
сомнительные ячейки не подсвечиваются, но подсказка с `text` и уверенностью есть.
#### Scenario: Догадка подтверждена проверкой
- **WHEN** значение `is_recognized = false`, `needs_review = false`
- **THEN** ячейка не подсвечена; в подсказке сырой текст и уверенность
### Requirement: Правка строк, колонок и подтверждение в режиме «Редактировать»
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
колонки без даты (`period_kind = none`) или с предупреждением о дате (в том числе подтверждение той же даты); снятие подтверждения ячейки и кода (`confirmed:
false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем
же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
`LINES_UNRESOLVED` показывается тем же текстом, 409 `DUPLICATE_FORM` — со ссылкой на подтверждённую форму, 409
`LINE_CODE_TAKEN` — у поля кода. Кнопка «Подтвердить данные» учитывает строки `unreadable` / `missing` с суммой или
нераспознанным текстом в ячейках.
#### Scenario: Назначить код
- **WHEN** аналитик вводит 1250 в ячейку кода строки `unreadable` и сохраняет
- **THEN** уходит `PATCH` с `lines: [{line_id, line_code: "1250"}]`; после ответа строка встаёт на место 1250
#### Scenario: Подтверждение заблокировано
- **WHEN** у формы две строки `missing` с суммами
- **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой
### Requirement: Предупреждения ОСВ-документа
Экран ОСВ-документа SHALL показывать предупреждения документа из `DocumentOut.issues[]` (коды `OSV_*`) блоком над
таблицей; все записи относятся к документу (строки ОСВ `line_ids` не адресуются); уровень `error` отличается от `info`.
#### Scenario: Предупреждение листа ОСВ
- **WHEN** у ОСВ-документа в `issues` есть запись с кодом `OSV_*`
- **THEN** над таблицей показан блок предупреждений с текстом записи и уровнем

View File

@ -0,0 +1,11 @@
## ADDED Requirements
### Requirement: Предупреждения файла и документа в карточке файла
Карточка файла (панель файлов) SHALL читать `job.warnings` как список структур `{code, severity, message, locator}`
(после выкладки ядра v5.19 это не строки; у старых заданий ядро отдаёт строки, обёрнутые в `LEGACY_WARNING`), показывать
`message` и `locator`, отличая `error` от `info`; предупреждения листа ОСВ и документа — из `DocumentOut.issues[]`.
`job.mapping_warnings` остаётся списком строк.
#### Scenario: Потерянный ОФР в карточке
- **WHEN** у задания `job.warnings = [{code: "JSON_FAILED", severity: "error", message: "…", locator: "file:x_ofr.json"}]`
- **THEN** карточка показывает предупреждение уровня «ошибка» с текстом и местом; экран не падает на объекте вместо строки

View File

@ -0,0 +1,81 @@
# consolidated-osv-ui Specification
## Purpose
Аналитик видит сводную ОСВ версии [30]: сводку с несопоставленными строками, пробелы по кварталам, таблицу с фильтрами в заголовках колонок и экспорт в xlsx; сопоставляет контрагента строки с ЮЛ справочника [32].
## Requirements
### Requirement: Сводка версии
Экран «Сводная ОСВ» [30] SHALL показывать шапку версии (ГК, период, номер, статус) и сводку
`GET /consolidations/versions/{id}/osv/summary`: строк всего, документов, несопоставленных (`unmapped_total` — счётчик
«Осталось N» — строки без сопоставленного ЮЛ и без ИНН из файла, arm-core #74; сопоставление — «Указать» в строке),
автоматически сопоставленных.
#### Scenario: Контрольные цифры РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3 после подтверждения 22 ОСВ из `fixtures/osv/{rb,schastl}/2024-Q{1,2,3}`
- **THEN** «строк» = 1385, «документов» = 22, «несопоставленных» = 1260
### Requirement: Таблица свода и фильтры
Экран SHALL показывать строки `GET /consolidations/versions/{id}/osv` постранично (размер страницы ≤ 2000): ЮЛ, счёт,
контрагент, ИНН контрагента, сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт (суммы — строками с
разделителями разрядов) и последней колонкой «Тип контрагента» — бейдж ГСК (синий) / ГК (зелёный) / Внешний (серый), как
в макете [30]. Фильтры — в заголовках колонок: ЮЛ (`legal_entity_id`), корень счёта (`account_root`), поиск по
контрагенту (`q`, ≤ 500 символов), тип контрагента (`counterparty_type`, одно значение — на колонке «Тип контрагента»);
только несопоставленные (`unmapped`) — кнопкой «Показать ЮЛ без ИНН» в баннере и воронкой колонки «ИНН» (подсвечена,
пока фильтр включён, макет [32-osv-no-inn]). «Применить» и «Сбросить» SHALL закрывать окно фильтра.
Смена фильтра SHALL перечитывать первую страницу.
#### Scenario: Строка из контрольных цифр
- **WHEN** фильтр ЮЛ «Пекарня Счастливочная» и поиск «бисквит»
- **THEN** строка `60.01` показывает сальдо нач. Кт 4 454 151.93, обороты Дт 128 058 143.65, Кт 125 950 450.77,
сальдо кон. Кт 2 346 459.05, признак «ГСК»
### Requirement: Ручное сопоставление контрагента
Ячейка ИНН (моноширинный шрифт, по центру) SHALL показывать, как в макете [32-osv-no-inn]:
- ИНН из файла (`counterparty_inn`) — просто текстом, сопоставлена строка или нет (строка с ИНН сопоставления не требует,
решение владельца 08.10.2026, отмена П-1);
- строка сопоставлена, ИНН в файле нет — ИНН ЮЛ справочника (`counterparty_entity_inn`) кнопкой-«таблеткой»
(`osv-inn-mapped-button`); нажатие открывает ту же модалку для пересопоставления, у текущего ЮЛ вместо «Связать» —
неактивное «Связано»;
- нет ни ЮЛ, ни ИНН — подсвеченная ячейка со ссылкой «Указать» (`osv-inn-link-button`).
«Указать» и «таблетка» SHALL открывать модалку
«Выбор организации для связи с <имя контрагента из файла>» [32-osv-map-counterparty]: поиск, таблица ИНН / Организация /
ГК / ГСК / ГСЗ / «Связать», рекомендуемые (`recommended`) — первыми под заголовком «Рекомендуемые» с зелёной подсветкой.
Кандидаты — `GET …/counterparty-candidates?scope=&entry_id=&q=` с паузой 300 мс после ввода: по умолчанию `scope=group`
(периметр и ГК); галочка «Искать во всём справочнике» — `scope=all`, при поиске короче 3 символов запрос не отправляется
и показывается подсказка.
«Связать» SHALL отправлять `POST …/osv/entries/{entry_ids.at(-1)}/counterparty {legal_entity_id}`, после успеха —
сообщение (с кем связано, сколько записей ОСВ сопоставлено автоприменением, создан или перенесён алиас), закрывать
модалку и перечитывать страницу свода и сводку («Осталось N» на клиенте не вычисляется). Ответы 409:
- `ALIAS_TAKEN` — подтверждение «Имя уже закреплено за <владельцы>. Перенести?» → повтор с `move_alias = true`;
- `LOCK_REQUIRED` — подтверждение «Взять версию в работу?» → `POST …/lock` → повтор; отказ — ничего не меняется;
- `LOCKED_BY_OTHER` (от правки или от `POST …/lock`) — «Версию держит <имя>» (имя — из ответа `POST …/lock` или из
`lock` карточки версии), без перехвата;
- `VERSION_NOT_EDITABLE` и прочие ошибки — текстом сервера.
#### Scenario: Сопоставление «Пекарня Счастливочная ООО» в ОСВ РБ
- **WHEN** в своде РБ 2024 Q3 у строки контрагента «Пекарня Счастливочная ООО» без сопоставления нажато «Указать» и у
рекомендуемой «Пекарни Счастливочной» — «Связать» (версия не взята в работу → «Взять в работу»)
- **THEN** строка показывает ЮЛ и тип «ГСК», «Осталось N» уменьшается на число сопоставленных строк, у ЮЛ появляется алиас
#### Scenario: Алиас занят
- **WHEN** имя строки уже алиас другого ЮЛ и аналитик выбирает третье ЮЛ
- **THEN** показан вопрос о переносе с именами владельцев; «Перенести» повторяет запрос с `move_alias = true`
### Requirement: Пробелы
Экран SHALL показывать `gaps[]` сводки списком «ЮЛ — счёт — недостающие кварталы» с пояснением, что строки этих пар в
своде отсутствуют.
#### Scenario: Пробелы РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3
- **THEN** показано 7 пробелов, у каждого недостающие кварталы «Q1, Q2»
### Requirement: Экспорт
Кнопка «Скачать xlsx» SHALL получать `GET /consolidations/versions/{id}/osv/export` с текущими фильтрами через клиент API
с токеном и сохранять файл; имя — из `Content-Disposition`, а если заголовок недоступен —
`Сводная_ОСВ_<ГК>_<год>_<квартал>.xlsx`.
#### Scenario: Экспорт с фильтром
- **WHEN** аналитик скачивает свод с фильтром по ЮЛ
- **THEN** сохраняется xlsx, а ошибка сервера показывается текстом, а не скачивается

View File

@ -0,0 +1,44 @@
# consolidation-setup Specification
## Purpose
Аналитик создаёт консолидацию ГК за период из модалки [42] (периметр — члены ГСК с возможностью правки), сразу попадает в карточку новой версии и из любой версии переходит к её сводной ОСВ.
## Requirements
### Requirement: Состав модалки «Новая консолидация»
Модалка [42] SHALL предлагать выбор ГК (`GET /directories/company-groups`), ГСК этой ГК с `kind = gsk`
(`GET /directories/company-groups/{id}/related-groups`), года и квартала, и показывать **все** незакрытые ЮЛ ГК
(`GET /directories/legal-entities?company_group_id=&closed=false`, страницы дочитываются по `offset`, пока не получены
все `total`). Члены выбранной ГСК (`GET /directories/related-groups/{id}` → `members[]`) SHALL быть отмечены
автоматически; пользователь может снять или добавить отметки.
#### Scenario: Выбор ГСК отмечает её членов
- **WHEN** аналитик выбирает ГК «Русский бисквит» и её ГСК
- **THEN** в списке ЮЛ видны все ЮЛ ГК, а члены ГСК («АО "Русский бисквит"», «ООО "Пекарня Счастливочная"») отмечены
#### Scenario: ЮЛ больше одной страницы
- **WHEN** у ГК больше ЮЛ, чем `limit` одной страницы
- **THEN** модалка показывает все ЮЛ ГК, а не первую страницу
### Requirement: Создание и переход к версии
По «Создать» модалка SHALL отправить `POST /consolidations {company_group_id, related_group_id, year, quarter,
legal_entity_ids}` с отмеченными ЮЛ. При 201 модалка SHALL закрыться, список консолидаций ГК — перечитаться, а созданная
версия (`VersionCard.id`) — открыться в карточке. При ошибке модалка SHALL остаться открытой и показать `message` из
`ApiError` (коды `CONSOLIDATION_EXISTS`, `RELATED_GROUP_MISMATCH`, `ENTITY_NOT_IN_GROUP`, `PERIMETER_TOO_SMALL` и 422
валидации).
#### Scenario: Успешное создание
- **WHEN** аналитик создаёт консолидацию 2024 Q3 с периметром из ГСК
- **THEN** открыта карточка версии №1 со статусом «Инициация» и двумя ЮЛ в панели «Организации»
#### Scenario: Консолидация за период уже есть
- **WHEN** сервер отвечает 409 `CONSOLIDATION_EXISTS`
- **THEN** модалка не закрывается и показывает текст ошибки сервера
### Requirement: Переход к сводной ОСВ из карточки
Кнопка «К отчётности» карточки версии SHALL вести на `/groups/:group_id/versions/:version_id/osv` и быть доступна в
любом статусе версии.
#### Scenario: Переход к своду
- **WHEN** аналитик нажимает «К отчётности» у версии
- **THEN** открывается экран сводной ОСВ этой версии

View File

@ -0,0 +1,185 @@
# document-review-ui Specification
## Purpose
Аналитик проверяет распознанный документ и подтверждает его: форма ББ/ОФР рядом со сканом PDF (правка значений и шапки, проверки и их акцепт) [41]/[41-02], ОСВ-документ — таблицей строк в порядке файла (как в 1С) только для чтения.
## Requirements
### Requirement: Экран документа
Маршрут `/documents/:document_id` SHALL загрузить `GET /documents/{id}` и показать вид по `document_type`: `BALANCE` и
`INCOME` — проверка формы, `OSV` — просмотр ОСВ; для прочих типов — карточку документа без данных. Шапка экрана SHALL
показывать тип, ЮЛ-владельца, период и статус документа, кнопку «Назад».
#### Scenario: Открытие формы баланса
- **WHEN** аналитик открывает документ `BALANCE`
- **THEN** показан экран проверки формы
### Requirement: Скан рядом с формой
Для ББ/ОФР экран SHALL показывать исходный файл документа: PDF — постранично через pdf.js из байт
`GET /files/{file_id}/content?disposition=inline`, полученных клиентом API с токеном (не через `<iframe>` и не прямой
ссылкой); изображение (png/jpg) — тегом `<img>` из blob; xlsx — без превью, с кнопкой «Скачать исходник».
#### Scenario: PDF под CSP sandbox
- **WHEN** сервер отдаёт PDF с `Content-Security-Policy: sandbox`
- **THEN** страницы PDF отрисованы на canvas и листаются
### Requirement: Форма ББ/ОФР — просмотр и правка
Экран SHALL показывать `GET /documents/{id}/statement`: шапку (ИНН, дата отчёта, единицы — значение и исходное при
правке; наименование, ОКУД/ОКПО) и таблицу «строка (код, наименование) × колонки» с суммами из `values[]`, включая
строки и ячейки, достроенные сервером по справочнику редакции (`id = null`). Ячейки с `needs_review = true` SHALL
подсвечиваться как «требует внимания» (подсветки напрямую по `confidence < 100` или `is_recognized = false` нет), с
`is_modified = true` — как «исправлено» с исходным значением `original_amount` в подсказке; пустая достроенная ячейка не
подсвечивается. Строки «БАЛАНС» формы `BALANCE` (коды 1600 и 1700) SHALL выделяться фоном `#f9f0ff` и жирным шрифтом;
подсветка ячейки перекрывает фон строки. Единицы (`header.units`) SHALL выводиться над таблицей. Пока документ не
`CONFIRMED`, ячейка SHALL быть редактируемой: ввод проверяется по формату суммы (до 2 знаков после точки, знак минус
допустим, пустое — `null`), сохранение отправляет `PATCH /documents/{id}/statement {values: [...]}` суммой-строкой:
ячейка строки из БД — `{line_id, column_id, amount}`, ячейка достроенной строки (`id = null`) — `{line_code,
column_id, amount}`; ответ заменяет состояние экрана. Правка шапки (ИНН, дата отчёта, единицы, наименование
организации) SHALL отправляться тем же `PATCH` в `header`; `inn: null` не отправляется. Правка строк, колонок и
подтверждений в том же `PATCH` (`columns`, `lines`, `values[].confirmed`) описана в требовании «Правка строк, колонок и
подтверждение в режиме «Редактировать»».
#### Scenario: Исправление ячейки
- **WHEN** аналитик меняет значение ячейки и сохраняет
- **THEN** ячейка помечена «исправлено», проверки пересчитаны по ответу сервера, акцепт снят
#### Scenario: Заполнение строки, которой не было в распознавании
- **WHEN** у формы нет строки 1700 из источника, аналитик вводит сумму в достроенную строку 1700 и сохраняет
- **THEN** уходит `{line_code: "1700", column_id, amount}`, по ответу строка получила `id`, ячейка помечена «исправлено»
#### Scenario: Строки «БАЛАНС»
- **WHEN** открыта форма `BALANCE`
- **THEN** строки 1600 и 1700 выделены цветом и жирным, у ОФР выделения нет
#### Scenario: Неверный формат суммы
- **WHEN** аналитик вводит «12,3,4»
- **THEN** запрос не отправляется, поле показывает ошибку формата
### Requirement: Проверки и их акцепт
Экран SHALL показывать `checks`: жёсткие (`failed`), мягкие (`soft_failed`), межформенную (`cross_form`) и акцепт
(`accepted`: кто, когда, комментарий). При непустом `failed` и `accepted = null` SHALL быть доступно «Принять с
комментарием»: комментарий 1–2000 символов → `POST /documents/{id}/statement/checks/accept {comment}`.
#### Scenario: Акцепт нарушений
- **WHEN** есть жёсткие нарушения и аналитик принимает их с комментарием
- **THEN** блок проверок показывает акцепт, нарушения остаются видимыми
### Requirement: Подтверждение документа
Кнопка «Подтвердить данные» SHALL отправлять `POST /documents/{id}/confirm` и быть недоступна для `CONFIRMED` и `ERROR`, а
также при наличии строк `unreadable` / `missing` с суммой или нераспознанным текстом в ячейках (подсказка «Разберите
строки без кода: N» с переходом к первой такой строке). При 409 `CHECKS_NOT_PASSED` экран SHALL показать
`detail.failures[]` и предложить акцепт; при 409 `PERIOD_REQUIRED` — подсказать исправить дату отчёта; при 409
`LINES_UNRESOLVED` — показать тот же текст, что у отключённой кнопки; при 409 `DUPLICATE_FORM` — со ссылкой на
подтверждённую форму; прочие ошибки — `message`. После успеха экран SHALL перейти в режим только чтения.
#### Scenario: Подтверждение с неакцептованными нарушениями
- **WHEN** аналитик подтверждает форму с жёсткими нарушениями без акцепта
- **THEN** показан список нарушений из ответа и кнопка «Принять с комментарием»
#### Scenario: Дубль подтверждённой формы
- **WHEN** сервер отвечает 409 `DUPLICATE_FORM`
- **THEN** экран показывает ошибку со ссылкой на уже подтверждённую форму
### Requirement: ОСВ-документ
Для `OSV` экран SHALL показывать строки `GET /documents/{id}/entries` (все страницы, `limit` ≤ 2000) таблицей в порядке
файла, как в 1С (AURORA-1702): строки счёта, субсчетов и подразделений, у которых есть вложенные строки, — строки-группы
(жирным, со своими сальдо и оборотами), вложенные строки — с отступом по уровню в колонке аналитики. Договоры отдельными
строками не идут — их сумма в строке контрагента; субсчёт или подразделение без вложенных строк и без сумм не выводится.
Строки-показатели колонки «Показатели» и строки «Кол.», «Вал.» (§1.3 правил предметной области: `group_type = OTHER`,
`data_type` не BU/SUM и не равен виду родителя — NU, QUANTITY, CURRENCY или пусто у «ПР»/«ВР») в рублёвую таблицу
SHALL NOT попадать и родителя группой не делают: строка БУ остаётся строкой с наименованием и суммами в рублях
(AURORA-1703). Колонки: субсчёт, подразделение (если в файле есть этот уровень), аналитика (у строки счёта — её текст),
ИНН (если есть контрагенты), сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт (суммы — строками с
разделителями разрядов, рубли), только чтение. Фильтр по счёту оставляет счёт и все его субсчета («62» — 62, 62.01,
62.02), при поиске по аналитике или ИНН строки-группы скрываются. Кнопка «Подтвердить» — как для форм.
Заголовок колонки аналитики SHALL зависеть от вида строк-листьев (структурные строки субсчёта и подразделения,
строки-группы и строки-показатели не учитываются, договоры строками не идут): только `COUNTERPARTY` — «Контрагент»; один другой вид — его `group_name`
(«Основные средства»); смесь видов или вид без `group_name` — «Аналитика», а перечень видов — во всплывающей подсказке
заголовка. Колонка «ИНН» и подсказка поиска «Контрагент или ИНН» SHALL показываться только при наличии контрагентов
(`COUNTERPARTY`); иначе колонки ИНН нет, а поиск подписан «Поиск по аналитике».
#### Scenario: ОСВ по основным средствам
- **WHEN** листья документа — аналитика с `group_name` «Основные средства», контрагентов нет
- **THEN** колонка называется «Основные средства», колонки ИНН нет, поиск подписан «Поиск по аналитике»
#### Scenario: Строки счёта и субсчетов
- **WHEN** аналитик открывает ОСВ по счёту 62 с субсчетами 62.01 и 62.02 и контрагентами под ними
- **THEN** в таблице жирным видны строки 62, 62.01, 62.02 с их сальдо и оборотами, контрагенты — под своим субсчётом с отступом
#### Scenario: ОСВ с колонкой «Показатели»
- **WHEN** у позиции номенклатуры счёта 41 в файле строки «БУ» и «Кол.»
- **THEN** в таблице строка позиции с наименованием и суммами в рублях, строки «Кол.» нет
#### Scenario: Подтверждение ОСВ
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`
### Requirement: Виртуализация таблицы ОСВ-документа
Таблица строк ОСВ-документа SHALL быть виртуализирована: в DOM находятся только видимые строки, остальные не
отрисовываются. Строки по-прежнему загружаются целиком, поиск по аналитике (и ИНН) и фильтр по счёту — клиентские;
серверной пагинации нет. Высота области прокрутки считается числом по высоте окна и пересчитывается при его изменении.
Строка «Итого по документу» закреплена внизу и выровнена по колонкам, двухъярусная шапка и колонка «Субсчёт»
закреплены, горизонтальная прокрутка сохраняется. ОСВ любого размера (десятки тысяч строк) SHALL открываться без
падения вкладки.
#### Scenario: Большая ОСВ
- **WHEN** аналитик открывает ОСВ-документ на ~29 000 строк
- **THEN** документ открывается, число строк в DOM не зависит от размера документа, прокрутка до конца, поиск и фильтр по субсчёту работают, «Итого по документу» совпадает с суммой по всему документу
### Requirement: Раскладка строк формы со статусами
Экран проверки формы ББ/ОФР ([41]) SHALL сохранять раскладку «как на бумаге» (Р-14): разделы, внутри — строки `ok` по
возрастанию кода, несколько строк одного кода — в порядке файла (`row_index`), шаблонные строки на своих местах,
пятизначные подстроки под родителем. Строка `unreadable` / `missing` SHALL стоять под ближайшей предыдущей по
`row_index` строкой, размещённой по коду, а без такой — в начале первого раздела; она подсвечена целиком, в ячейке
кода серым — сырой текст `line_code_text` с подсказкой. Строки `detail` SHALL быть скрыты под переключателем
«Показать расшифровки (N)» и показываться под родителем (`parent_row_index`) с отступом. Экран показывает справочное
имя строки (`reference_name`), файловое (`line_name`) — рядом, если отличается. Переключателя «как в файле» нет.
#### Scenario: Код-мусор на своём месте
- **WHEN** строка `unreadable` «ИЧ,250» в файле стоит после строки 1240
- **THEN** на экране она стоит сразу под 1240, подсвечена, в ячейке кода серым «ИЧ,250»
#### Scenario: Расшифровки скрыты
- **WHEN** у формы три строки `detail`
- **THEN** по умолчанию их не видно, переключатель «Показать расшифровки (3)»; включённый показывает их под родителями
### Requirement: Подсветка и предупреждения у места
Экран SHALL подсвечивать ячейки только по `needs_review` (в подсказке — `text` и уверенность), строки — по
`needs_review` строки, поля шапки — по `needs_review_fields` (в подсказке у поля — `header.source.<поле>`: сырой текст и
уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек
(`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` —
столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized =
false` напрямую нет. У значений строк `detail` `needs_review` всегда `false` (Р-23): при показанных расшифровках их
сомнительные ячейки не подсвечиваются, но подсказка с `text` и уверенностью есть.
#### Scenario: Догадка подтверждена проверкой
- **WHEN** значение `is_recognized = false`, `needs_review = false`
- **THEN** ячейка не подсвечена; в подсказке сырой текст и уверенность
### Requirement: Правка строк, колонок и подтверждение в режиме «Редактировать»
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
колонки без даты (`period_kind = none`) или с предупреждением о дате (в том числе подтверждение той же даты); снятие подтверждения ячейки и кода (`confirmed:
false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем
же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
`LINES_UNRESOLVED` показывается тем же текстом, 409 `DUPLICATE_FORM` — со ссылкой на подтверждённую форму, 409
`LINE_CODE_TAKEN` — у поля кода. Кнопка «Подтвердить данные» учитывает строки `unreadable` / `missing` с суммой или
нераспознанным текстом в ячейках.
#### Scenario: Назначить код
- **WHEN** аналитик вводит 1250 в ячейку кода строки `unreadable` и сохраняет
- **THEN** уходит `PATCH` с `lines: [{line_id, line_code: "1250"}]`; после ответа строка встаёт на место 1250
#### Scenario: Подтверждение заблокировано
- **WHEN** у формы две строки `missing` с суммами
- **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой
### Requirement: Предупреждения ОСВ-документа
Экран ОСВ-документа SHALL показывать предупреждения документа из `DocumentOut.issues[]` (коды `OSV_*`) блоком над
таблицей; все записи относятся к документу (строки ОСВ `line_ids` не адресуются); уровень `error` отличается от `info`.
#### Scenario: Предупреждение листа ОСВ
- **WHEN** у ОСВ-документа в `issues` есть запись с кодом `OSV_*`
- **THEN** над таблицей показан блок предупреждений с текстом записи и уровнем

View File

@ -0,0 +1,77 @@
# primary-files-ui Specification
## Purpose
Аналитик загружает первичные файлы ЮЛ в контексте версии и видит в панели «Первичные документы» [39]/[40] файлы и их документы со статусами распознавания; список обновляется сам, пока идёт распознавание.
## Requirements
### Requirement: Загрузка файлов ЮЛ в контексте версии
Панель «Файлы ЮЛ» [39]/[40], открытая из строки ЮЛ карточки версии, SHALL загружать выбранные файлы одним запросом
`POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (multipart, поле `files` повторяется),
где `consolidation_id` — консолидация открытой версии (из списка консолидаций ГК в сторе по `current_version_id`),
без других полей формы и без query `consolidation_version_id`. Тип и период документа не спрашиваются. Кнопка
«Загрузить документы» SHALL быть недоступна с подсказкой «Загрузка доступна только в версии в работе», если статус
открытой версии не `INITIATED`, `DOCUMENTS_PROCESSING` или `DATA_CORRECTION`. После ответа панель SHALL показать итог:
принятые файлы (`accepted[].name`) и отклонённые с причиной (`rejected[].name` + `message`). Ответ 422 с телом
`UploadResult` SHALL трактоваться как «ни один файл не принят» с тем же показом `rejected[]`; ответ 422 с `detail`
(`ENTITY_NOT_IN_PERIMETER`), 409 (`CONSOLIDATION_NOT_EDITABLE`) и прочие ошибки — показом `message`.
#### Scenario: Часть файлов отклонена
- **WHEN** аналитик загружает ОСВ-файл и файл, уже загруженный ранее у этого ЮЛ
- **THEN** первый показан принятым, второй — отклонённым с текстом причины `DUPLICATE_FILE` от сервера
#### Scenario: Все файлы отклонены
- **WHEN** сервер отвечает 422 с телом `UploadResult`
- **THEN** панель показывает каждый отклонённый файл с причиной, а не общую ошибку
#### Scenario: Загрузка идёт в консолидацию
- **WHEN** панель открыта из версии консолидации 2024 Q3 и аналитик загружает файл
- **THEN** запрос уходит на `/consolidations/{id консолидации 2024 Q3}/legal-entities/{id ЮЛ}/files` без query; после
приёма файл появляется в столе панели
#### Scenario: Версия не в работе
- **WHEN** панель открыта из версии в статусе `CANCELLED`
- **THEN** кнопка «Загрузить документы» недоступна, при наведении — подсказка
### Requirement: Список файлов и документов ЮЛ
Панель SHALL по умолчанию показывать рабочий стол консолидации открытой версии —
`GET /consolidations/{consolidation_id}/legal-entities/{id}/files` (новые первыми): имя, статус файла по-русски
(«Обработка» для `UPLOADED`/`PROCESSING`, «Ошибка» с причиной из `job` для `ERROR`) и вложенные `documents[]` — тип
(«ОСВ <счёт>», «Бух. баланс», «ОФР», «Расшифровка», «Прочее»), период («2024 К1», «2024 · 9 мес.», «период не
определён»), статус документа («Требуется проверка», «Загружен» для `CONFIRMED`, «Ошибка»). Документы `OSV`, `BALANCE`,
`INCOME` SHALL быть ссылками на `/documents/:document_id`. Переключатель «Все документы организации» SHALL переключать
панель на `GET /legal-entities/{id}/files`; в этом режиме у файла не из текущей консолидации в подписи SHALL стоять его
консолидация: «<год> К<n>» по списку консолидаций ГК, «вне консолидации» при `consolidation_id = null`, «другая
консолидация», если id нет в списке. Если консолидация открытой версии не определена (версии нет в списке ГК), панель
SHALL работать в режиме «все» без переключателя.
#### Scenario: PDF с двумя формами
- **WHEN** файл `aep_6.pdf` распознан
- **THEN** под файлом видны два документа — «Бух. баланс» и «ОФР» со статусом «Требуется проверка», каждый открывает
экран проверки
#### Scenario: Стол и все документы
- **WHEN** у ЮЛ есть файлы, загруженные в консолидации 2024 Q3 и 2024 Q4, и файл, загруженный без контекста; панель
открыта из версии 2024 Q3
- **THEN** по умолчанию виден только файл 2024 Q3; после переключателя «Все документы организации» видны все три, у
файла 2024 Q4 подпись «2024 К4», у файла без контекста — «вне консолидации», у файла 2024 Q3 подписи нет
### Requirement: Опрос до окончания распознавания
Пока панель открыта и среди файлов активного списка (стол или все документы) есть `UPLOADED` или `PROCESSING`, панель
SHALL перечитывать этот список раз в 5 секунд и SHALL прекратить опрос, когда таких файлов нет или панель закрыта.
Переключение режима или смена ЮЛ SHALL сбрасывать список и перечитывать его. При закрытии панели карточка версии SHALL
перечитываться (обновить ячейки чек-листа).
#### Scenario: Опрос останавливается
- **WHEN** все файлы активного списка перешли в `PROCESSED` или `ERROR`
- **THEN** запросы списка файлов прекращаются
### Requirement: Предупреждения файла и документа в карточке файла
Карточка файла (панель файлов) SHALL читать `job.warnings` как список структур `{code, severity, message, locator}`
(после выкладки ядра v5.19 это не строки; у старых заданий ядро отдаёт строки, обёрнутые в `LEGACY_WARNING`), показывать
`message` и `locator`, отличая `error` от `info`; предупреждения листа ОСВ и документа — из `DocumentOut.issues[]`.
`job.mapping_warnings` остаётся списком строк.
#### Scenario: Потерянный ОФР в карточке
- **WHEN** у задания `job.warnings = [{code: "JSON_FAILED", severity: "error", message: "…", locator: "file:x_ofr.json"}]`
- **THEN** карточка показывает предупреждение уровня «ошибка» с текстом и местом; экран не падает на объекте вместо строки

27
web/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,27 @@
# Архитектура фронта arm-core/web
SPA поверх API arm-core (`/api/v1`). Своих данных и бизнес-вычислений нет: всё производное (чек-лист, сводная ОСВ,
статусы) считает сервер, фронт показывает и отправляет действия пользователя. До 21.10.2026 фронт вёлся в отдельном
репозитории `RSHB/arm-web` и переносился сюда синхронизацией; теперь — только здесь.
## Модули
| Модуль | Ответственность | Зависит от |
|---|---|---|
| `src/api/` | HTTP-клиент, токен, разбор ошибок | `conf/`, `auth/token` |
| `src/auth/` | профиль `/me`, dev-вход, права для меню | `api/` |
| `src/services/api/` | вызовы ресурсов arm-core | `api/`, `types/` |
| `src/store/` | состояние экранов (Redux Toolkit) | `services/api/` |
| `src/utils/` | чистые функции: деньги, подписи, скачивание | `types/` |
| `src/types/` | DTO API (повторяют `../api/src/schemas/`) | — |
| `src/components/` | примитивы UI (таблица, теги, заголовок) | antd |
| `src/widgets/` | блоки экранов (`DocumentReview` — проверка документа со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer`) | `components/`, `store/`, `services/api/`, `utils/` |
| `src/layouts/` | страницы-маршруты | `widgets/` |
## Границы
- Сетевые запросы — только через `src/api/client.ts`; `fetch` напрямую не вызывается.
- `components/` не знают про store и API; `types/` ни от чего не зависят.
- `utils/` не импортирует React, store и API.
## Сборка
`npm run build` → `web/dist`, Dockerfile ядра кладёт его в `api/web/` и FastAPI отдаёт SPA (`../README.md`).

67
web/CLAUDE.md Normal file
View File

@ -0,0 +1,67 @@
# CLAUDE.md — arm-core/web (фронт АРМ КА)
Общий контекст — `../../CLAUDE.md` (workspace) и `../CLAUDE.md` (arm-core). Этот файл — только про фронт.
Язык общения, коммитов, комментариев и документации — русский; идентификаторы — английские, как в DBML и API.
## Что это
SPA кредитного аналитика АРМ КА: справочники групп, консолидации ГК по периодам, карточка версии с чек-листом
документов, загрузка и проверка первичных файлов (ОСВ, ББ/ОФР из PDF), сводная ОСВ и шаги консолидации.
Бэкенд — `../api` (`/api/v1`, единственный источник данных). С 21.10.2026 фронт ведётся здесь: задачи — worktree arm-core,
ветка `feat/<issue>-<slug>`, PR в `main`. Прежний репозиторий `RSHB/arm-web` заморожен на `test` `de4e477`
(= arm-core `3157a15`), в нём осталась история (номера вида `arm-web#7` в комментариях кода — оттуда). Открытые issue
arm-web перенесены в arm-core #150–#157 (arm-web#7 → #151), спеки экранов и архив их изменений — в `../openspec/`
(архивы с суффиксом `-web` — UI-половины изменений, у которых бэкенд-половина лежит под тем же именем без суффикса).
## Источники истины для фронта
1. OpenAPI arm-core и гайд `../../docs/dev/frontend-api-guide.md` — эндпоинты, тела, коды ошибок. Типы в `src/types/`
повторяют схемы бэка (`../api/src/schemas/`), поля не придумываем.
2. Пользовательский путь — `../../docs/context/10-user-journey.md`; макет — `../../docs/mockup/` (по одному экрану через
`../../docs/mockup/INDEX.md`).
3. Права — данные: доступ решает сервер (403). Меню пока не скрывается по правам (`Sidebar` не применяет permission);
коды прав из `/me` лежат в `src/auth/permissions.ts`.
- Требования к экранам: действующие — `../openspec/specs/*-ui/spec.md` (перенесены из arm-web@de4e477), новые и
изменённые — `../openspec/changes/` этого репозитория.
- `../../reference-old-arm/` — только чтение: инфраструктурные идеи (PDFResultViewer) можно смотреть, эндпоинты старого
АРМ (`/fsl/...`, `/osv/by-file-id`, `POST /files`) в arm-core не существуют.
## Стек и ограничения
- React 18 + TypeScript strict, rsbuild, antd 6 (основной UI-кит; daisyui/tailwind — из шаблона), react-router 7,
Redux Toolkit, pdfjs-dist (legacy-сборка — для Яндекс Браузера, см. `src/widgets/DocumentReview/pdfjs.ts`).
- Пакеты `raisa-*` — `optionalDependencies` (вне банка недоступны); `package-lock.json` в git не хранится (в банке
зависимости ставятся через Nexus). В контуре `package.json` урезается до сборки
(`../../docs/dev/contour-transfer-guide.md` §3.2): новые пакеты, нужные сборке, добавлять и в разрешённый список там.
- env только `REACT_APP_*` через `src/conf/config.ts`; `REACT_APP_API_URL` — адрес arm-core, `REACT_APP_ROOT_PATH` =
`ROOT_PATH` бэка.
## Архитектура
Карта модулей и границы — `ARCHITECTURE.md`. Главное:
- `src/api/` — единый HTTP-клиент `apiFetch` / `api.get|post|put|patch|del`: префикс `${API_URL}/api/v1`, Bearer-токен,
`ApiError {status, code, message}` по трём формам `detail`, событие `armka:unauthorized` на 401. **Все запросы — только
через него** (прямой `fetch` теряет токен и разбор ошибок). `apiFetchBlob(path) → {blob, filename}` — бинарные ответы
(скан PDF, экспорт xlsx) с той же обработкой токена/401/ошибок.
- `src/services/api/` — функции по ресурсам arm-core (consolidations, files, documents, osv, directories/…).
- `src/utils/` — чистые функции без React/store/API: деньги (`money.ts`, суммы — строки), подписи статусов/типов/периодов
(`labels.ts`), скачивание и имя файла (`download.ts`).
- `src/auth/` — `AuthGate` (профиль `/me`), `DevLogin` (`POST /dev/token`, только не в production), `token.ts`.
- `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` — файлы ЮЛ), `src/components/` — примитивы
(`GenericTable`, `TableTag`, `PageTitle`), `src/types/` — DTO API.
## Команды (из `web/`)
- `npm install`; `npm test` — vitest; `npm run lint` — ESLint + `prettier --check`; `npm run format` — **перед PR**
(`eslint --fix` → `biome check --write` → `prettier --write`).
- `REACT_APP_ROOT_PATH=/ npm run start` — dev-сервер на :3000 (без переменной приложение ждёт префикс
`/aurora/apps/fastapi-dkb-armka` и открывается белый экран) с proxy `/api` → `ARM_CORE_URL` (по умолчанию
`http://localhost:8000`); бэкенд — локальный стенд по `../../docs/dev/qa-guide.md` §4.
- Из корня arm-core: `make web-check` (install + biome + `tsc --noEmit` + build, входит в `check-full`). Перед PR фронта
зелёными должны быть `make web-check`, `npm test` и `npm run lint`; вывод — в описание PR.
## Тесты
Unit-тесты — vitest на чистых функциях (`src/**/*.test.ts`); компоненты не тестируются — ручной сценарий по стенду с
фикстурами из `../../fixtures` (`../../docs/dev/qa-guide.md` §8, `../../docs/scripts/demo-prepare.sh`). Экран сверять с
макетом и проходить сценарий в браузере до сдачи.
## Для владельца
Пользователь слаб во фронте: на TS/React-изменения агент делает код-ревью сам и объясняет решения простым языком.

View File

@ -1,34 +1,35 @@
# arm-web — фронт АРМ КА (демо-прототип)
# arm-core/web — фронт АРМ КА
SPA кредитного аналитика поверх API `arm-core` (`/api/v1`). Контекст системы — `../CLAUDE.md`, архитектура —
`ARCHITECTURE.md`, соглашения репозитория — `CLAUDE.md`.
SPA кредитного аналитика поверх API `arm-core` (`/api/v1`). Контекст системы — `../../CLAUDE.md`, ядро — `../README.md`,
архитектура фронта — `ARCHITECTURE.md`, соглашения — `CLAUDE.md`. С 21.10.2026 фронт ведётся здесь (PR в arm-core
`main`); прежний репозиторий `RSHB/arm-web` заморожен.
## Запуск
1. `make install`.
2. Стенд arm-core — по `../docs/dev/qa-guide.md` §4 (ядро на `localhost:8000`, `DEBUG=true`, `make seed-dev`); для
1. `npm install` (из `web/`).
2. Стенд arm-core — по `../../docs/dev/qa-guide.md` §4 (ядро на `localhost:8000`, `DEBUG=true`, `make seed-dev`); для
распознавания PDF — профиль `services` (§6.8: tabular-parser, scan-gateway, scan-emulator).
3. `make start` — dev-сервер rsbuild (сам ставит `REACT_APP_ROOT_PATH=/`; без этого приложение ждёт префикс
3. `REACT_APP_ROOT_PATH=/ npm run start` — dev-сервер rsbuild (без `REACT_APP_ROOT_PATH=/` приложение ждёт префикс
`/aurora/apps/fastapi-dkb-armka` и показывает белый экран, см. `qa-guide` §4.3). Запросы `/api` проксируются на
`ARM_CORE_URL` (по умолчанию `http://localhost:8000`), `REACT_APP_API_URL` в dev не нужен.
4. Вход — через dev-форму (`POST /dev/token`), например `ka_go@armka.local`.
## Форматирование и линтеры
**Перед PR — `npm run format`** (или `make format`): `eslint --fix` → `biome check --write` (импорты, безопасные
**Перед PR — `npm run format`**: `eslint --fix` → `biome check --write` (импорты, безопасные
исправления) → `prettier --write`. Команда приводит `src/` к единому виду; повторный запуск ничего не меняет.
- Prettier (`.prettierrc.json`) — единственный форматтер: табы, ширина 140, одинарные кавычки (в CSS — двойные),
`>` тега JSX на той же строке. Форматтер biome выключен, чтобы два инструмента не спорили о пробелах.
- ESLint (`eslint.config.js`) — правила, повторяющие `biome.json`; biome остаётся линтером в `make lint`/`make check`.
- ESLint (`eslint.config.js`) — правила, повторяющие `biome.json`; biome остаётся линтером в `make web-check` ядра.
- `npm run lint` — `eslint` + `prettier --check` без исправлений: ошибки валят команду, предупреждения (`any`,
`console`, зависимости хуков) — нет.
- В редакторе: расширения Prettier и ESLint, «Format on Save» с Prettier — результат совпадёт с `npm run format`.
## Проверка
`make check` — install, biome + ESLint + Prettier (`make lint`), `tsc --noEmit`, vitest, `rsbuild build`. Зелёным
считается всё вместе.
Из корня arm-core — `make web-check` (install, biome, `tsc --noEmit`, `rsbuild build`; входит в `check-full`), из `web/` —
`npm test` (vitest) и `npm run lint` (ESLint + Prettier). Зелёным считается всё вместе.
## Демо-сценарий
@ -43,9 +44,9 @@ SPA кредитного аналитика поверх API `arm-core` (`/api/v
`CONSOLIDATION_EXISTS`) → члены ГСК отмечены → «Создать» → открывается страница группы с новой версией.
2. Дальше работаем на подготовленной версии 2024 Q3 (`/groups/<id>?version=0437f8be-019c-4dbb-9776-08c306aa38cc`):
панель «Организации» → ЮЛ → «Файлы» → документы ОСВ в статусе «Загружен». Загрузка ОСВ вручную —
`../fixtures/osv/{rb,schastl}/2024-Q{1,2,3}/*`; для новой версии быстро — `scripts/demo-prepare.sh <version_id> [email]`
(каталог фикстур — `FIXTURES_DIR`, по умолчанию `../../fixtures` относительно репозитория; в конце печатает сводку).
3. ББ/ОФР: открыть уже загруженный ББ Счастливочной (или загрузить `../fixtures/ocr/aep_6.pdf` РБ) → скан слева, форма справа,
`../../fixtures/osv/{rb,schastl}/2024-Q{1,2,3}/*`; для новой версии быстро — `../../docs/scripts/demo-prepare.sh <version_id> [email]`
(каталог фикстур — `FIXTURES_DIR`, по умолчанию `fixtures` workspace; в конце печатает сводку).
3. ББ/ОФР: открыть уже загруженный ББ Счастливочной (или загрузить `../../fixtures/ocr/aep_6.pdf` РБ) → скан слева, форма справа,
исправить ячейку. ИНН и дата отчёта в aep_6 не распознаются: в шапке ввести дату строго `2024-09-30` (ГГГГ-ММ-ДД) и
нажать «Сохранить», иначе «Подтвердить данные» даст 409 `PERIOD_REQUIRED`. Принять нарушения с комментарием (если есть), подтвердить.
4. «К отчётности» → сводная ОСВ; фильтр «Счастливочная» + «бисквит» → строка `60.01` (сальдо нач. Кт 4 454 151.93,
@ -62,13 +63,11 @@ SPA кредитного аналитика поверх API `arm-core` (`/api/v
(«Показать все»); в режиме правки можно назначить код строке, пометить её расшифровкой и подтвердить значение, код или дату
колонки — всё уходит одним `PATCH`; кнопка «Подтвердить данные» заблокирована, пока есть строки без кода (ссылка
«Разберите строки без кода: N» ведёт к первой); панель файлов ЮЛ показывает счётчик предупреждений файла и документа.
Карта модулей — в `CLAUDE.md`, раздел «Архитектура».
Карта модулей — `ARCHITECTURE.md`.
## Ограничения прототипа
## Ограничения
- Нет блокировки версии («Взять в работу»), сопоставления контрагентов, экрана чек-листа, комментариев, удаления файлов,
смены периметра и ручных переходов статусов.
- Нет правки строк ОСВ (в API нет), картинок страниц и координат ячеек в скане (в API нет).
- Нет генерации типов из OpenAPI и автотестов компонентов (vitest — только чистые функции).
- Нет сборки под контур и переноса в `arm-core/web` — отдельной задачей после демо.
- Изменений arm-core / scan-gateway нет: найденные пробелы — отдельные issue в тех репозиториях.