Слияние с #39 (PR #40): scroll.x считается от фактических колонок, scroll.y — числом для virtual. Колонки сумм расширены до 160 px: итоги из 17 цифр («Итого по документу») переносились на две строки и обрезались внизу таблицы (O-3). Спека: поиск — по аналитике. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
11 KiB
Markdown
104 lines
11 KiB
Markdown
# document-review-ui Specification
|
||
|
||
## Purpose
|
||
Аналитик проверяет распознанный документ и подтверждает его: форма ББ/ОФР рядом со сканом PDF (правка значений и шапки, проверки и их акцепт) [41]/[41-02], ОСВ-документ — плоской таблицей строк только для чтения.
|
||
|
||
## 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`). Ячейки с `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** запрос не отправляется, поле показывает ошибку формата
|
||
|
||
### 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) плоской
|
||
таблицей строк нижнего уровня (договоры отдельными строками не идут — их сумма в строке контрагента) с колонками:
|
||
субсчёт, подразделение (если в файле есть этот уровень), аналитика, ИНН (если есть контрагенты), сальдо на начало Дт/Кт,
|
||
обороты Дт/Кт, сальдо на конец Дт/Кт (суммы — строками с разделителями разрядов, рубли), только чтение. Кнопка «Подтвердить» — как для форм.
|
||
Заголовок колонки аналитики SHALL зависеть от вида строк-листьев (структурные строки субсчёта и подразделения не
|
||
учитываются, договоры строками не идут): только `COUNTERPARTY` — «Контрагент»; один другой вид — его `group_name`
|
||
(«Основные средства»); смесь видов или вид без `group_name` — «Аналитика», а перечень видов — во всплывающей подсказке
|
||
заголовка. Колонка «ИНН» и подсказка поиска «Контрагент или ИНН» SHALL показываться только при наличии контрагентов
|
||
(`COUNTERPARTY`); иначе колонки ИНН нет, а поиск подписан «Поиск по аналитике».
|
||
|
||
#### Scenario: ОСВ по основным средствам
|
||
- **WHEN** листья документа — аналитика с `group_name` «Основные средства», контрагентов нет
|
||
- **THEN** колонка называется «Основные средства», колонки ИНН нет, поиск подписан «Поиск по аналитике»
|
||
|
||
#### Scenario: Подтверждение ОСВ
|
||
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
|
||
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`
|
||
|
||
### Requirement: Виртуализация таблицы ОСВ-документа
|
||
Таблица строк ОСВ-документа SHALL быть виртуализирована: в DOM находятся только видимые строки, остальные не
|
||
отрисовываются. Строки по-прежнему загружаются целиком, поиск по аналитике (и ИНН) и фильтр по субсчёту — клиентские;
|
||
серверной пагинации нет. Высота области прокрутки считается числом по высоте окна и пересчитывается при его изменении.
|
||
Строка «Итого по документу» закреплена внизу и выровнена по колонкам, двухъярусная шапка и колонка «Субсчёт»
|
||
закреплены, горизонтальная прокрутка сохраняется. ОСВ любого размера (десятки тысяч строк) SHALL открываться без
|
||
падения вкладки.
|
||
|
||
#### Scenario: Большая ОСВ
|
||
- **WHEN** аналитик открывает ОСВ-документ на ~29 000 строк
|
||
- **THEN** документ открывается, число строк в DOM не зависит от размера документа, прокрутка до конца, поиск и фильтр по субсчёту работают, «Итого по документу» совпадает с суммой по всему документу
|