diff --git a/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/design.md b/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/design.md
new file mode 100644
index 0000000..52b451c
--- /dev/null
+++ b/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/design.md
@@ -0,0 +1,97 @@
+## Context
+
+Экран «Шахматка» [31-01] добавлен в `arm-core/web` PR #158 (ветка `feat/chessTable`, AURORA-1538) как вторая вкладка страницы
+отчёта по версии. Страница `layouts/ConsolidatedOsvPage.tsx` разделена: общую шапку и вкладки держит
+`layouts/ConsolidationReportPage.tsx`, содержимое вкладок — `widgets/ConsolidatedOsvContent` (прежняя сводная ОСВ без изменений
+поведения) и `widgets/ConsolidatedChessContent` (матрица `ConsolidationChessTable` и результирующая таблица
+`ConsolidationChessResTable`). Бэкенд — change `2026-10-16-intercompany-matrix` (матрица, результирующая таблица) и
+`2026-10-22-intercompany-result-table-export` (выгрузка результирующей таблицы); расчёт — чистая функция версии, ничего не
+хранится.
+
+## Decisions
+
+- **D-1. Маршрут `:tab`.** `OsvTabs` берёт активную вкладку из `useParams().tab` (по умолчанию `osv`) и по выбору заменяет
+ последний сегмент адреса. Неизвестный `tab` заменяется на `osv` (`navigate(…, {replace: true})`).
+- **D-2. Ключ версии.** Страница отчёта разделена на обёртку с `key={version_id}` и содержимое: при смене версии карточка,
+ фильтры и данные создаются заново (как было в `ConsolidatedOsvPage`).
+- **D-3. Режим расчёта из выбора счетов:** `manual` — при непустых Дт и Кт, `all` — при пустых обоих; при одной выбранной
+ стороне запроса нет (бэкенд ответил бы 422 на `manual`, а `all` показал бы не то, что выбрано). Тот же признак выбирает
+ маршрут экспорта.
+- **D-4. Единицы, знаки и фильтр компаний — на клиенте.** Запрос повторяется только при смене выбора счетов или версии.
+ Единицы — сдвиг запятой в десятичной строке (`utils/chessTableCellFormatValue.ts`), `Intl.NumberFormat` получает строку и
+ форматирует её как точное десятичное (без float).
+- **D-5. План счетов** — `GET /directories/accounts` целиком, деревья Дт и Кт строятся на клиенте по `parent_id`.
+- **D-6. `apiFetchBlob(path, init)`** принимает `RequestInit`, чтобы выгружать POST-ом с телом; без `init` поведение прежнее.
+- **D-7. Полноэкранный режим** — Fullscreen API на контейнере блока, состояние следует за `fullscreenchange`; выпадающие окна
+ antd рендерятся в `document.fullscreenElement` (`ConfigProvider getPopupContainer`).
+- **D-8. Итоги — задача бэкенда.** Решение владельца 22.10.2026: агрегаты (строки «Итого») должен отдавать API. Перенос —
+ технический долг: AURORA-1756 (итоги и правило суммирования в API, ждёт аналитику AURORA-1373) и AURORA-1757 (вывод итогов из
+ API). До этого строки «Итого» остаются клиентскими (решение владельца: «особо не мешает»): точное сложение строк через
+ `sumAmounts`, под матрицей — `amount_a` по колонке, под результирующей таблицей — все статьи по колонке; колонка «Итого»
+ результирующей таблицы — `group_total` из ответа.
+- **D-9. `useNotificationApi` мемоизирован** (`useMemo` по `notification` из `App.useApp()`, который antd сам мемоизирует):
+ объект уведомлений стабилен между рендерами, поэтому `loadData`/`loadResData` — `useCallback([notification])`, а эффекты
+ загрузки зависят от них без лишних перезапросов.
+
+## Known issues
+
+Открытая проблема на момент слияния (решение владельца 23.10.2026 — сливать, остаток в техдолг AURORA-1799):
+
+1. Выбрана одна сторона Дт/Кт: «Экспорт» недоступен, но таблица показывает прежние данные, подсказка — всплывающее уведомление
+ и постоянный текст внутри выпадающего окна, текста у таблицы и у фильтров нет. Целевое поведение — решение (ж).
+
+Пункты раунда 3 про поиск в дереве счетов и стили закрыты ревьюером (решение (п)).
+
+## Decisions made without discussion
+
+- **2026-10-21 (а). Спеку пишет ревьюер в ветке автора** — по решению владельца (разрешено явно); альтернатива — оставить
+ автору. Цена ошибки: автор может не согласиться с формулировками; правится одним коммитом в этом же change.
+- **2026-10-21 (б). Спека описывает фактическое поведение, дефекты вынесены в «Known issues».** Отменено 22.10.2026 (е). Обоснование: спека — единственный
+ источник истины при рулингах, а закреплять баги требованиями нельзя; иначе их закрытие превратится в «нарушение спеки». Цена
+ ошибки: отдельные требования (например, режим `all` при выборе одной стороны, «Итого» из расхождений) описаны как есть
+ и после исправления потребуют MODIFIED.
+- **2026-10-21 (в). При восстановлении затёртого фронта** (коммит 175343b) из `origin/main` взяты целиком все файлы без
+ шахматочных изменений, а в `App.tsx`, `layouts/index.ts`, `api/client.ts` (параметр `init`), `OsvTabs.tsx`,
+ `ConsolidatedOsvContent.tsx` оставлены изменения шахматки. Возвращён `key={version_id}` (разделением страницы на обёртку и
+ содержимое — поведение как в main). Обоснование: изменения PR в этих файлах относятся к шахматке; цена ошибки — незамеченная
+ откатившаяся правка main в этих пяти файлах (построчно сверено с `origin/main`, расхождений кроме шахматочных нет).
+- **2026-10-21 (г). Механические правки biome без смены поведения** (коммит e19291c): тип `IntercompanyMatrixResponse` у `let res`,
+ `Number.isNaN`, `key` у `span` в `OsvTabs`, `?? null` вместо пустого фрагмента. Предупреждения eslint (8 новых в коде шахматки:
+ `any`, deps хуков, `console`) не исправлялись — они входят в Known issues. Цена ошибки: `key` на `span` внутри `Tooltip`
+ безвреден, но избыточен.
+- **2026-10-21 (д). Дельта к `consolidated-osv-ui` не делается:** вкладки описаны в новой спеке, текст
+ `consolidated-osv-ui` не правился, чтобы не смешивать UI-половины. Цена ошибки: при архивации две спеки частично описывают
+ один компонент (`OsvTabs`).
+- **2026-10-22 (е). Спека переписана на целевое поведение** (раунд 3 ревью): владелец решил, что спеки и тесты чужих PR ведём
+ мы, а автор правит продуктовый код; целевая спека даёт автору однозначный ориентир, расхождения кода — в «Known issues».
+ Цена ошибки: до исправлений спека и код ветки расходятся; PR не сливается, пока «Known issues» не закрыты.
+- **2026-10-22 (ж). Выбрана одна сторона Дт/Кт — пустая таблица с текстом** «Для расчёта по счетам выберите счета и в Дт,
+ и в Кт» у таблицы и у фильтров, «Экспорт» матрицы недоступен. Альтернатива — оставить прежние данные с подсказкой; отвергнута:
+ на экране были бы данные, не соответствующие фильтру. Цена ошибки: правка текста и одного условия.
+- **2026-10-22 (з). Правило отметок дерева по полному плану счетов** (родитель отмечен ⇔ отмечены все потомки), в том числе при
+ поиске. Обоснование: бэкенд разворачивает выбранный счёт в потомков, лишняя отметка родителя меняет расчёт. Цена ошибки: если
+ аналитик захочет «родитель + выбранные субсчета» как отдельный режим — MODIFIED требования.
+- **2026-10-22 (и). Запасные имена файлов** `Шахматка_<ГК>_<год>_<квартал>.xlsx` и
+ `Результирующая_таблица_ВГО_<ГК>_<год>_<квартал>.xlsx`. Цена ошибки: низкая, имя из `Content-Disposition` приоритетнее.
+- **2026-10-22 (к). Тесты форматтера** `utils/chessTableCellFormatValue.test.ts` написаны ревьюером: сдвиг единиц, точное
+ округление, большие суммы, «-0,00», пустые и нечисловые значения при любых единицах.
+- **2026-10-22 (л). В ветку влит `main`** (PR #159), чтобы выгрузка результирующей таблицы работала на стенде из ветки.
+- **2026-10-22 (м). Желательные пункты ревью закрыты ревьюером** (решение владельца): форматтер (проверка числа до сдвига
+ запятой, `Intl.StringNumericLiteral` через `ES2023.Intl` в `lib` tsconfig вместо `as any`, без мёртвого разбора скобок),
+ запасные имена (`utils/download.xlsxFilename`), отдельные флаги выгрузки, зависимости эффектов (D-9), «—» вместо выдуманного
+ нуля, ячейки по макету, «Год»/«Период» версии только для чтения, сервис `services/api/intercompanyMatrix.ts` с `withJsonBody`,
+ типы шахматки в `types/api.ts`, `Account.kind: 'A' | 'P' | 'AP' | null`. Цена ошибки: конфликты слияния у автора в
+ `ConsolidatedChessContent.tsx` и `ConsolidationChessTableFilter.tsx` — он правит те же файлы по «Known issues»; автору сказано
+ начать с `git pull`.
+- **2026-10-22 (н). «-0,00» убирается проверкой готовой строки, а не опцией `signDisplay: 'negative'`.** Обоснование: на движке
+ без Intl.NumberFormat v3 неизвестное значение `signDisplay` бросает `RangeError` и ломает весь экран, а проверка строки
+ безопасна везде. Цена ошибки: нет.
+- **2026-10-22 (о). Расхождение в скобках всегда**, в том числе нулевое — `(0,00)`: в API `variance_amount` — модуль разности
+ (неотрицательный), скобки в макете 31-01 — способ показа третьей строки, а не знак. Значок пропущенных строк ОСВ — оранжевый
+ `WarningOutlined` у первого числа. Подпись «Период» (не «Квартал») — как в макете. Цена ошибки: правка вида ячейки.
+- **2026-10-23 (п). Остаток раунда 3 закрыт ревьюером, пункт про одну сторону Дт/Кт — в техдолг** (решение владельца: «доправим
+ сами, потом сольём»). Отметки дерева — чистая функция `utils/treeChecks.toggleTreeCheck` (правило (з) снизу вверх по полному
+ дереву на всех уровнях; скрытые поиском отметки берутся из состояния выпадашки, а не из применённого фильтра — иначе снятые
+ отметки возвращались), тесты `utils/treeChecks.test.ts`. Правило тегов из `App.css` перенесено в
+ `ConsolidationChessTableFilter.css`, класс с формы ЮЛ снят. Пустая таблица с подсказкой при одной стороне — AURORA-1799.
+ Цена ошибки: при одной стороне до AURORA-1799 на экране прежние данные, выгрузка недоступна.
diff --git a/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/proposal.md b/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/proposal.md
new file mode 100644
index 0000000..4bb57d4
--- /dev/null
+++ b/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/proposal.md
@@ -0,0 +1,49 @@
+## Why
+
+В ветке `feat/chessTable` (PR RSHB/arm-core#158, автор — Голов) во фронте появился экран «Шахматка» — вкладка страницы
+отчёта по версии консолидации рядом со «Сводной ОСВ». Бэкенд шахматки (`GET/POST …/intercompany-matrix/*`) уже в main
+(change `2026-10-16-intercompany-matrix`), у экрана спеки нет. Спеку и тесты ведёт ревьюер в ветке автора по решению
+владельца (21–22.10.2026): спека задаёт целевое поведение экрана, расхождения кода ветки с ней — `design.md`, «Known issues».
+
+## What Changes
+
+Репозиторий — только arm-core (`web/`). Экран «Шахматка» [31-01], эндпоинты arm-core:
+`GET …/intercompany-matrix/all`, `POST …/intercompany-matrix/manual`, `GET …/intercompany-matrix/all/export`,
+`POST …/intercompany-matrix/manual/export`, `GET …/intercompany-matrix/result-table`,
+`GET …/intercompany-matrix/result-table/export`, `GET /directories/accounts`.
+
+- Маршрут `/groups/:group_id/versions/:version_id/:tab`: страница отчёта (`ConsolidationReportPage`) с общей шапкой и
+ вкладками (`OsvTabs`); `tab = osv` — сводная ОСВ, `tab = chess` — шахматка. Прежний адрес `…/osv` продолжает работать.
+- Таблица ЮЛ × ЮЛ: в ячейке три числа (сумма A, сумма Б, расхождение); итоги считает бэкенд (пока API их не отдаёт —
+ строки «Итого» нет).
+- Результирующая таблица ВГО по статьям с колонкой «Итого» (`group_total`) и выгрузкой в XLSX.
+- Фильтры: компании, счета Дт, счета Кт (деревом), единицы, знаков после запятой; режимы расчёта `all` / `manual`.
+- Экспорт в XLSX (`all` / `manual`, результирующая таблица), полноэкранный режим.
+- Новая спека `intercompany-matrix-ui` (ADDED Requirements).
+
+## Non-goals
+
+- Исправление продуктового кода по «Known issues» (`design.md`) — за автором PR; спеку и тесты ведёт ревьюер.
+- Пресеты счетов — во фронте не реализованы.
+- Итоги «Итого» в API и их вывод — AURORA-1756, AURORA-1757.
+- Выбор года и квартала на экране: селекторы заглушены (`disabled`), версия задаётся адресом.
+- Изменения бэкенда и DBML: шахматка — чистая функция от версии, ничего не хранится (DECISION-5).
+- Вкладки «Таблица корректировки», «Гудвилл», «КФО», «КФО Общий», «Подтверждение КФО клиента» — недоступны.
+
+## Capabilities
+
+### New Capabilities
+- `intercompany-matrix-ui`: экран «Шахматка» версии консолидации.
+
+### Modified Capabilities
+
+Нет: `consolidated-osv-ui` не правится (вкладки описаны в новой спеке, см. design.md).
+
+## Impact
+
+- `web/src/App.tsx`, `web/src/layouts/{ConsolidationReportPage,index}.ts(x)`, `web/src/widgets/ConsolidatedOsv/OsvTabs.tsx`,
+ `web/src/widgets/ConsolidatedOsvContent/`, `web/src/widgets/ConsolidatedChessContent/`,
+ `web/src/widgets/ConsolidationChessTable/`, `web/src/widgets/ConsolidationChessResTable/`,
+ `web/src/widgets/ConsolidationChessFormatTableFilter/`, `web/src/components/TreeFilterDropdown/`,
+ `web/src/utils/chessTableCellFormatValue.ts`, `web/src/services/consodilation/intercompanyMatrix.ts`, `web/src/types/chess/chess.ts`, `web/src/api/client.ts` (`apiFetchBlob` принимает `init`).
+- Права: экран читает данные под `CONSOLIDATION_VIEW`; меню по правам не скрывается.
diff --git a/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/specs/intercompany-matrix-ui/spec.md b/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/specs/intercompany-matrix-ui/spec.md
new file mode 100644
index 0000000..cb2b870
--- /dev/null
+++ b/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/specs/intercompany-matrix-ui/spec.md
@@ -0,0 +1,179 @@
+## ADDED Requirements
+
+### Requirement: Вкладки страницы отчёта по версии
+Страница `/groups/:group_id/versions/:version_id/:tab` SHALL показывать шапку версии (ГК, период, статус отчёта) один раз
+и под ней ряд вкладок шагов консолидации и содержимое активной вкладки. Активная вкладка определяется сегментом `:tab`:
+`osv` — «ОСВ» (сводная ОСВ), `chess` — «Шахматка». Выбор вкладки SHALL менять последний сегмент адреса на ключ вкладки
+(навигация, не локальное состояние), активная вкладка подсвечена зелёным и выделена жирным. Неизвестный ключ вкладки SHALL
+заменяться на `osv` без новой записи в истории браузера. Вкладки «Таблица корректировки», «Гудвилл», «КФО», «КФО Общий»,
+«Подтверждение КФО клиента» SHALL быть недоступны с подсказкой «Будет в следующих срезах». При смене `version_id` всё
+состояние страницы (карточка версии, данные, фильтры) SHALL создаваться заново.
+
+#### Scenario: Переход на шахматку
+- **WHEN** открыта сводная ОСВ версии по адресу `…/versions/{id}/osv` и нажата вкладка «Шахматка»
+- **THEN** адрес становится `…/versions/{id}/chess`, шапка версии остаётся, под вкладками показана шахматка
+
+#### Scenario: Прежний адрес сводной ОСВ
+- **WHEN** открыт адрес `…/versions/{id}/osv`
+- **THEN** показана сводная ОСВ, вкладка «ОСВ» активна
+
+#### Scenario: Неизвестная вкладка
+- **WHEN** открыт адрес `…/versions/{id}/abc`
+- **THEN** адрес заменяется на `…/versions/{id}/osv`, показана сводная ОСВ; кнопка «Назад» браузера не возвращает на `…/abc`
+
+### Requirement: Таблица шахматки ЮЛ × ЮЛ
+Экран SHALL загружать матрицу при открытии вкладки (`GET /consolidations/versions/{id}/intercompany-matrix/all`) и показывать
+таблицу: первая колонка «ДБ / КТ» (закреплена слева) — компании периметра в порядке ответа (`companies[]`), остальные колонки —
+те же компании. В ячейке пары (строка A, колонка Б) SHALL быть три числа друг под другом: сумма `amount_a`, сумма `amount_b`
+и расхождение `variance_amount` в скобках; расхождение выделяется красным, если `exceeds_threshold = true` или
+`one_sided = true` (пара без встречной стороны; правило подсветки — как в API и xlsx, AURORA-1651). Если у пары есть
+строки ОСВ без сопоставленного счёта (`skipped_osv_entry_count > 0`), рядом с первым числом SHALL показываться значок
+предупреждения с подсказкой «Строк ОСВ без сопоставленного счёта: N; в суммы не вошли». Если ячейки пары в ответе нет
+(в том числе на диагонали), SHALL показываться прочерк «—» на приглушённом фоне. Ниже таблицы закреплена строка «Итого»: до
+появления итогов в API (AURORA-1756, AURORA-1757) она временно считается на клиенте точным сложением десятичных строк — сумма
+`amount_a` по колонке видимых ячеек; итог, который не удалось сложить, показывается прочерком «—», а не нулём. Пока идёт
+загрузка, таблица показывает индикатор загрузки; ошибка API показывается уведомлением с текстом сервера. Панель фильтров
+показывает год и квартал версии неактивными полями «Год» и «Период» (например, `2024` и `К1`): версия задаётся адресом.
+
+#### Scenario: Ячейка с расхождением выше порога
+- **WHEN** для пары `(A, Б)` сервер вернул `exceeds_threshold = true`
+- **THEN** третье число ячейки (расхождение) показано красным, первые два — обычным цветом
+
+#### Scenario: Пара без встречной стороны
+- **WHEN** для пары `(A, Б)` сервер вернул `amount_b = 0`, `one_sided = true`, `exceeds_threshold = false`
+- **THEN** третье число ячейки (расхождение) показано красным
+
+#### Scenario: Пары нет в ответе
+- **WHEN** в `cells[]` нет ячейки для пары компаний
+- **THEN** на их пересечении показан прочерк «—»
+
+#### Scenario: Пропущенные строки ОСВ
+- **WHEN** у ячейки `skipped_osv_entry_count = 2`
+- **THEN** у первого числа ячейки виден значок предупреждения, подсказка — «Строк ОСВ без сопоставленного счёта: 2; в суммы не вошли»
+
+### Requirement: Результирующая таблица ВГО
+Под матрицей экран SHALL показывать результирующую таблицу ВГО, загруженную при открытии вкладки
+(`GET /consolidations/versions/{id}/intercompany-matrix/result-table`): первая колонка «Статья» — статьи ответа (`articles[]`,
+`name`) в порядке ответа, далее колонки компаний (`companies[]`) с суммой статьи по компании (`amounts[]` по `company_id`) и
+колонка «Итого» со значением `group_total` из ответа; пустая сумма — прочерк «—». Ниже таблицы закреплена строка «Итого»: до
+появления итогов в API (AURORA-1756, AURORA-1757) она временно считается на клиенте точным сложением сумм всех статей по
+колонке (итог, который не удалось сложить, — «—»). У таблицы свои выбор единиц и знаков (по правилам требования «Единицы и знаки», независимо от
+матрицы), кнопка «Экспорт» и кнопка «На весь экран». Пока идёт загрузка, таблица показывает индикатор; ошибка API (в том числе
+422 `RESULT_ACCOUNTS_MISSING`) показывается уведомлением с текстом сервера.
+
+#### Scenario: Колонка «Итого» из ответа
+- **WHEN** сервер вернул статью «Дебиторская задолженность» с `group_total = "1296510.00"`, выбраны «тыс. руб.» и 2 знака
+- **THEN** в колонке «Итого» этой статьи показано `1 296,51`
+
+#### Scenario: Нет обязательного счёта
+- **WHEN** сервер ответил 422 `RESULT_ACCOUNTS_MISSING`
+- **THEN** показано уведомление с текстом сервера, матрица шахматки при этом показывается как обычно
+
+### Requirement: Единицы и знаки
+Экран SHALL позволять выбрать единицы суммы — «руб.», «тыс. руб.» (по умолчанию), «млн. руб.», «млрд руб.» — и число знаков
+после запятой 0–3 (по умолчанию 2). Выбор применяется на клиенте к уже загруженным значениям (повторного запроса не делает):
+запятая сдвигается в десятичной строке на 3 / 6 / 9 разрядов, значение округляется до выбранного числа знаков (половина —
+от нуля) без перевода в число с плавающей точкой и показывается с разделителем разрядов и десятичной запятой. Значение,
+округлённое до нуля, показывается без минуса. Пустое значение — пустая ячейка. Значение, которое не удаётся прочитать как
+число, SHALL показываться как пришло — при любых единицах.
+
+#### Scenario: Смена единиц
+- **WHEN** выбраны «млн. руб.» и 1 знак, а сервер вернул `amount_a = "2346459.05"`
+- **THEN** в ячейке показано `2,3`
+
+#### Scenario: Точное округление
+- **WHEN** выбраны «тыс. руб.» и 2 знака, а сервер вернул `"1005"`
+- **THEN** показано `1,01` (не `1,00`, как дало бы округление числа с плавающей точкой)
+
+#### Scenario: Маленькая отрицательная сумма
+- **WHEN** выбраны «тыс. руб.» и 2 знака, а сервер вернул `"-4.00"`
+- **THEN** показано `0,00`, а не `-0,00`
+
+### Requirement: Фильтр компаний
+Выбор «Компании» (множественный, пустой выбор — «Все компании») SHALL ограничивать матрицу на клиенте: остаются строки и
+колонки выбранных компаний и ячейки, у которых обе компании выбраны. Запрос на сервер при этом не отправляется. Список
+вариантов — компании последнего загруженного ответа. Выпадающее окно выбора — дерево с поиском по названию, кнопками
+применения и сброса.
+
+#### Scenario: Две компании из периметра
+- **WHEN** выбраны две компании из пяти
+- **THEN** матрица содержит две строки и две колонки, «Итого» считается по оставшимся ячейкам
+
+### Requirement: Фильтр счетов Дт и Кт и режимы расчёта
+Экран SHALL загружать план счетов `GET /directories/accounts` один раз при открытии и строить из него два дерева по иерархии
+`parent_id`: дебетовые счета (`is_debit`) и кредитовые (`is_credit`); подпись узла — «код - название», в том числе у выбранных
+дочерних счетов в поле выбора. Выбор счетов Дт и Кт — множественный, пустой выбор — «Все счета».
+
+Отметки в дереве SHALL считаться по полному плану счетов: родительский счёт отмечен тогда и только тогда, когда отмечены все
+его потомки в полном дереве. Поиск по дереву скрывает не совпавшие узлы, но не меняет этого правила: отметка единственного
+видимого при поиске субсчёта не отмечает его родителя, а снятие отметки с субсчёта снимает её и с родителя. Скрытые поиском
+отметки при отметке видимых узлов сохраняются.
+
+Режим расчёта определяется выбором:
+- ничего не выбрано ни в Дт, ни в Кт — SHALL выполняться `GET …/intercompany-matrix/all` (режим `all`);
+- выбраны счета и в Дт, и в Кт — SHALL выполняться `POST …/intercompany-matrix/manual` с `{debit_account_ids,
+ credit_account_ids}` (режим `manual`);
+- выбрана только одна сторона — запрос SHALL NOT отправляться, таблица матрицы SHALL быть пустой и вместо данных показывать
+ текст «Для расчёта по счетам выберите счета и в Дт, и в Кт»; тот же текст SHALL показываться у фильтров Дт/Кт (не
+ всплывающим уведомлением) и только в этом состоянии; кнопка «Экспорт» матрицы SHALL быть недоступна.
+
+Каждое изменение выбора счетов, приводящее к режиму `all` или `manual`, перезапрашивает матрицу; на экране остаётся ответ на
+последний запрос, индикатор загрузки снимается и при переходе в состояние «выбрана одна сторона». Кнопка «Сбросить фильтры»
+появляется, когда выбрано что-либо в компаниях, Дт или Кт, и очищает все три выбора. Выбор единиц и знаков кнопкой не
+сбрасывается. Ошибка загрузки плана счетов показывается уведомлением.
+
+#### Scenario: Обе стороны выбраны
+- **WHEN** выбран счёт 60.01 в Дт и счёт 62.01 в Кт
+- **THEN** отправляется `POST …/intercompany-matrix/manual` с идентификаторами этих счетов и таблица показывает ответ
+
+#### Scenario: Выбрана одна сторона
+- **WHEN** выбран счёт только в Дт
+- **THEN** запрос матрицы не отправляется, таблица пуста и показывает «Для расчёта по счетам выберите счета и в Дт, и в Кт»,
+ тот же текст виден у фильтров, «Экспорт» матрицы недоступен, выбор Дт остаётся в фильтре
+
+#### Scenario: Выбрана одна сторона во время загрузки
+- **WHEN** выбраны Дт и Кт, запрос `manual` ещё выполняется, и пользователь очищает Кт
+- **THEN** индикатор загрузки снят, ответ прежнего запроса на экран не выводится, таблица в состоянии «выбрана одна сторона»
+
+#### Scenario: Поиск и отметка субсчёта
+- **WHEN** в дереве Дт введён поиск, видна ветка 60 с единственным субсчётом 60.02, и отмечен 60.02
+- **THEN** выбранным остаётся только 60.02; после сброса поиска счёт 60 и его прочие субсчета не отмечены
+
+#### Scenario: Снятие субсчёта отмеченного родителя
+- **WHEN** отмечен счёт 60 (отмечены 60, 60.01, 60.02) и снята отметка с 60.01
+- **THEN** выбранным остаётся 60.02, счёт 60 не отмечен
+
+#### Scenario: Сброс
+- **WHEN** выбраны компании и счета, нажато «Сбросить фильтры»
+- **THEN** все три выбора пусты, матрица перезапрошена в режиме `all`, кнопка исчезла
+
+### Requirement: Экспорт в XLSX
+Кнопка «Экспорт» матрицы SHALL скачивать XLSX в том же режиме, что и таблица: в режиме `manual` —
+`POST …/intercompany-matrix/manual/export` с телом `{debit_account_ids, credit_account_ids}`, в режиме `all` —
+`GET …/intercompany-matrix/all/export`; при выбранной одной стороне кнопка недоступна. Кнопка «Экспорт» результирующей таблицы
+SHALL скачивать `GET …/intercompany-matrix/result-table/export`. Файл сохраняется под именем из `Content-Disposition`, при его
+отсутствии — `Шахматка_<ГК>_<год>_<квартал>.xlsx` и `Результирующая_таблица_ВГО_<ГК>_<год>_<квартал>.xlsx` соответственно.
+Пока идёт выгрузка, в состоянии загрузки только нажатая кнопка; ошибка API показывается уведомлением. Фильтр компаний, единицы и
+знаки в запрос экспорта не передаются.
+
+#### Scenario: Экспорт по всем счетам
+- **WHEN** счета не выбраны и нажато «Экспорт» матрицы
+- **THEN** отправлен `GET …/intercompany-matrix/all/export`, браузер сохраняет файл `intercompany-matrix_….xlsx`
+
+#### Scenario: Экспорт результирующей таблицы
+- **WHEN** нажато «Экспорт» результирующей таблицы
+- **THEN** отправлен `GET …/intercompany-matrix/result-table/export`, кнопка «Экспорт» матрицы не переходит в состояние загрузки
+
+### Requirement: Полноэкранный режим
+Кнопка «На весь экран» матрицы и результирующей таблицы SHALL переводить свой блок (панель фильтров и таблицу) в
+полноэкранный режим средствами Fullscreen API с белым фоном и таблицей на высоту окна; в нём кнопка меняется на «Свернуть».
+Выпадающие списки фильтров и подсказки SHALL оставаться видимыми в полноэкранном режиме. Выход клавишей Esc SHALL возвращать
+обычный вид и подпись кнопки. Отказ браузера войти в полноэкранный режим показывается уведомлением.
+
+#### Scenario: Вход и выход
+- **WHEN** нажато «На весь экран», затем Esc
+- **THEN** блок разворачивается на весь экран, затем возвращается в страницу, кнопка снова «На весь экран»
+
+#### Scenario: Фильтр в полноэкранном режиме
+- **WHEN** матрица в полноэкранном режиме и открыт выбор «Компании»
+- **THEN** выпадающее окно видно поверх полноэкранного блока
diff --git a/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/tasks.md b/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/tasks.md
new file mode 100644
index 0000000..bc82b5f
--- /dev/null
+++ b/openspec/changes/archive/2026-10-23-intercompany-matrix-ui/tasks.md
@@ -0,0 +1,13 @@
+## 1. Спека и ревью (RSHB/arm-core#158, AURORA-1538)
+
+- [x] 1.1 Спека `intercompany-matrix-ui` (ревьюер; с раунда 3 — целевое поведение).
+- [x] 1.2 Тесты форматтера сумм `utils/chessTableCellFormatValue.test.ts` (ревьюер).
+- [x] 1.3 Желательные пункты ревью раунда 3 (ревьюер, решение (м) в `design.md`).
+- [x] 1.4 «Known issues» раунда 3: автор (6b956f7, 79d4008) и ревьюер (решение (п)); пункт про одну сторону Дт/Кт — 2.2.
+- [x] 1.5 Перед слиянием: `make web-check`, `npm test`, `npm run lint` — зелёные, вывод в описание PR.
+- [x] 1.6 Сверка экрана с макетом 31-01 / 31-01-01 в браузере (`docs/mockup/INDEX.md`), в том числе полноэкранный режим.
+
+## 2. После слияния
+
+- [ ] 2.1 Итоги «Итого» из API вместо клиентских — AURORA-1756 (бэк), AURORA-1757 (фронт).
+- [ ] 2.2 Одна сторона Дт/Кт — пустая таблица с подсказкой, без уведомления — AURORA-1799.
diff --git a/openspec/changes/consolidations-ui/design.md b/openspec/changes/consolidations-ui/design.md
index e2f6986..3d1b50a 100644
--- a/openspec/changes/consolidations-ui/design.md
+++ b/openspec/changes/consolidations-ui/design.md
@@ -4,7 +4,7 @@ PR RSHB/arm-core#166 (ветка `AURORA-1589`, автор — Степанов,
список консолидаций ГК [34] (`widgets/ConsoleTable/`), модалку «Взять в работу» и перехват [35]
(`widgets/ConsoleTable/LockModal.tsx`), панель «Организации» (`widgets/ConsoleLegalEntities/`), модалку «Новая
консолидация» [42] (`widgets/NewConsoleModal/`, строка ЮЛ — `CompanyOptionRow.tsx`), кнопку «Изменить статус» в шапке
-карточки версии [30] (`layouts/ConsolidatedOsvPage.tsx`) и модалку [33-01] (`widgets/StatusChangeModal/ChangeStatusModal.tsx`).
+карточки версии [30] (`layouts/ConsolidationReportPage.tsx` — общая страница отчёта с вкладками после #158) и модалку [33-01] (`widgets/StatusChangeModal/ChangeStatusModal.tsx`).
Бэкенд — спека `consolidations-api` (список с `in_work_only`, «Взять в работу, перехват, снятие», «Статусы Среза 4»); API не
меняется. В ветку добавлена вторая библиотека иконок `lucide-react` (иконки макета: меню «⋯», «Взять в работу», «К
отчётности», боковое меню).
@@ -70,16 +70,15 @@ PR RSHB/arm-core#166 (ветка `AURORA-1589`, автор — Степанов,
обрезки), «К3» через `quarterShortLabel`, опечатки исправлены, ФИО с инициалами без второй точки (`endSentence`).
10. ~~Пояснение под заголовком~~ — закрыто: `CONS_STATUS_DESCRIPTION` / `consStatusDescription` (тексты спеки; нет текста —
пусто, название статуса не повторяется).
-11. ~~Модалка [31-00]~~ — закрыто: `UnmappedInnModal` и проверка в `OsvTabs` (`needsUnmappedWarning`). Вкладки шагов
- пока неактивны, поэтому на стенде модалка проверена на временной копии с активной «Шахматкой» (решение (м)).
+11. ~~Модалка [31-00]~~ — закрыто: `UnmappedInnModal` и проверка в `OsvTabs` (`needsUnmappedWarning`); после влива
+ main (#158) срабатывает при уходе с «ОСВ» на «Шахматку» (решение (ц)).
12. ~~«+ Создать» при одном ЮЛ~~ — закрыто: `MIN_PERIMETER = 2`, кнопка неактивна, у поля — «Выберите не меньше двух ЮЛ».
13. ~~Модалка «Изменить статус»: загрузка без обработки ошибок, `comment.trim()` на `undefined`~~ — закрыто: модалка
карточку не грузит, получает её со страницы (`card`), после 200 страница берёт карточку из ответа (D-4);
`canSubmitStatusChange` принимает `undefined`; ошибки — текст сервера, модалка открыта.
14. ~~Тестов нет~~ — закрыто: `web/src/utils/consolidations.test.ts`, `formatScaledAmount` в `web/src/utils/money.test.ts`.
-15. **`lucide-react@0.263.1` в зеркале npm банка не проверен** (открыто). Пакет в `dependencies`, снимок контура его
- оставляет (`docs/dev/contour-transfer-guide.md` §3.2); если версии нет в Nexus, сборка в контуре упадёт. Вопрос автору
- PR / платформе.
+15. ~~`lucide-react@0.263.1` в зеркале npm банка не проверен~~ — закрыто 23.10.2026: автор PR подтвердил владельцу, что
+ пакет в зеркале есть. Пакет в `dependencies`, снимок контура его оставляет (`docs/dev/contour-transfer-guide.md` §3.2).
16. **Предупреждение браузера о несохранённых правках** — частично: сделано для трёх модалок экранов консолидаций
(требование «Предупреждение о несохранённых правках в модалках консолидаций», решение (л)). Открыто: нужно ли оно в
остальных формах приложения (справочники, проверка документов) и при переходе по меню вне модалок (у `BrowserRouter`
@@ -129,7 +128,7 @@ PR RSHB/arm-core#166 (ветка `AURORA-1589`, автор — Степанов,
модалке»; модалки этого PR — понятное место, маска модалки и так закрывает меню; формы других экранов и переход по
меню вне модалок требуют решения владельца (Known issues 16). Цена ошибки: если предупреждение нужно и в других формах —
подключить тот же хук; если не нужно вовсе — убрать три вызова.
-- **2026-10-23 (м). Модалка [31-00] — в `OsvTabs` с параметрами `onNavigate` / `enabledTabs`**; пока шагов нет, вкладки
+- **2026-10-23 (м). Модалка [31-00] — в `OsvTabs` с параметрами `onNavigate` / `enabledTabs`** (заменено решением (ц)); пока шагов нет, вкладки
неактивны и модалка не вызывается, на стенде она проверена на временной копии с активной «Шахматкой». Обоснование: PR
шахматки (#158) только передаёт переход, модалку не пишет. Цена ошибки: подключение вкладки — две строки.
- **2026-10-23 (н). Ошибки в модалках — текст сервера** (`ApiError.message`, по-русски) вместо карты фронтовых текстов;
@@ -156,3 +155,10 @@ PR RSHB/arm-core#166 (ветка `AURORA-1589`, автор — Степанов,
из `widgets/ConsoleTable/consts.ts` (там остались цвета бейджей). Цена ошибки: низкая.
- **2026-10-23 (х). Отступ секций бокового меню — `padding: 12px 0`** (у автора закомментирован `12px`): по вертикали —
как в макете [34], по горизонтали пункты остаются на одной линии с кнопкой «Новая консолидация». Цена ошибки: CSS.
+- **2026-10-23 (ц). Влив main после #158 — перенос в страницу отчёта с вкладками** (решение владельца: «вливаем main и
+ переносим сами»). #158 заменил `layouts/ConsolidatedOsvPage.tsx` на `layouts/ConsolidationReportPage.tsx` (шапка + вкладки
+ по адресу `/versions/:version_id/:tab`) и `widgets/ConsolidatedOsvContent/` (содержимое вкладки «ОСВ»). Шапка этого PR
+ (пояснение статуса, тег, «Изменить статус» и модалка [33-01]) перенесена в `ConsolidationReportPage`, «Единицы / Знаков»
+ — в `OsvContent`. Модалка [31-00] встроена в вкладки по адресу: `OsvTabs({ unmappedTotal })` спрашивает только при уходе
+ с «ОСВ», а `OsvContent` сообщает странице `unmapped_total` сводки через `onUnmappedChange` (при уходе с вкладки — 0);
+ параметры `onNavigate` / `enabledTabs` из (м) не нужны. Цена ошибки: если [31-00] нужна и на «Назад» — это решение (д).
diff --git a/openspec/changes/consolidations-ui/proposal.md b/openspec/changes/consolidations-ui/proposal.md
index 777d19c..3451022 100644
--- a/openspec/changes/consolidations-ui/proposal.md
+++ b/openspec/changes/consolidations-ui/proposal.md
@@ -63,7 +63,8 @@
- `web/src/widgets/ConsoleTable/` (`ConsoleTable.tsx`, `LockModal.tsx`, `config.tsx`, `consts.ts`, `flatten.ts`),
`web/src/widgets/StatusChangeModal/ChangeStatusModal.tsx`, `web/src/widgets/NewConsoleModal/`
(`NewConsoleModal.tsx`, `CompanyOptionRow.tsx`), `web/src/widgets/ConsoleLegalEntities/`,
- `web/src/layouts/ConsolidatedOsvPage.tsx`, `web/src/layouts/ConsolidationsList.tsx`,
+ `web/src/layouts/ConsolidationReportPage.tsx` (шапка, после влива #158), `web/src/widgets/ConsolidatedOsvContent/`
+ («Единицы»), `web/src/layouts/ConsolidationsList.tsx`,
`web/src/services/api/consolidations.ts`, `web/src/store/slices/consolidations/thunks.ts`, `web/src/types/`,
`web/src/widgets/Sidebar/` (иконки), `web/package.json` (`lucide-react`).
- Права: действия требуют `CONSOLIDATION_EDIT` на сервере; меню по правам не скрывается.
diff --git a/openspec/changes/consolidations-ui/tasks.md b/openspec/changes/consolidations-ui/tasks.md
index 780cc22..bcb42f1 100644
--- a/openspec/changes/consolidations-ui/tasks.md
+++ b/openspec/changes/consolidations-ui/tasks.md
@@ -19,8 +19,9 @@
`parseInWorkOnly`, `canTakeInWork`, `needsUnmappedWarning`, `endSentence`).
- [x] 2.4 Предупреждение браузера о несохранённых правках — в модалках экранов консолидаций (решение (л)); остальные
формы — вопрос владельцу (Known issues 16).
-- [ ] 2.5 Проверить наличие `lucide-react@0.263.1` в зеркале npm банка (Known issues 15) или заменить иконки на
- `@ant-design/icons` — вопрос автору PR.
+- [x] 2.5 Проверить наличие `lucide-react@0.263.1` в зеркале npm банка (Known issues 15) — есть, подтвердил автор PR.
+- [x] 2.8 Влив main после #158: шапка, «Единицы» и [31-00] перенесены в страницу отчёта с вкладками (решение (ц));
+ `tsc`, `biome`, `npm run lint`, `vitest`, `make web-check` — зелёные; сверка на стенде.
- [x] 2.6 Проверки в копии ветки с `lucide-react`: `tsc --noEmit`, `biome check`, `prettier --check`, `eslint` (0 ошибок,
предупреждений меньше, чем на main), `vitest run`, `npm run build` — зелёные.
- [x] 2.7 Сверка на стенде: список [34] (флажок, «Версия», «⋯»), [35] (свободная версия, перехват и гонка — с подменой
diff --git a/openspec/specs/intercompany-matrix-ui/spec.md b/openspec/specs/intercompany-matrix-ui/spec.md
new file mode 100644
index 0000000..632086c
--- /dev/null
+++ b/openspec/specs/intercompany-matrix-ui/spec.md
@@ -0,0 +1,184 @@
+# intercompany-matrix-ui Specification
+
+## Purpose
+Аналитик видит шахматку ВГО версии [31-01] (матрица Дт/Кт по компаниям с фильтрами компаний и счетов, единицами и полноэкранным режимом) и результирующую таблицу ВГО [31-01-01], выгружает обе в xlsx; суммы и итоги приходят с бэка, фронт их только форматирует.
+
+## Requirements
+
+### Requirement: Вкладки страницы отчёта по версии
+Страница `/groups/:group_id/versions/:version_id/:tab` SHALL показывать шапку версии (ГК, период, статус отчёта) один раз
+и под ней ряд вкладок шагов консолидации и содержимое активной вкладки. Активная вкладка определяется сегментом `:tab`:
+`osv` — «ОСВ» (сводная ОСВ), `chess` — «Шахматка». Выбор вкладки SHALL менять последний сегмент адреса на ключ вкладки
+(навигация, не локальное состояние), активная вкладка подсвечена зелёным и выделена жирным. Неизвестный ключ вкладки SHALL
+заменяться на `osv` без новой записи в истории браузера. Вкладки «Таблица корректировки», «Гудвилл», «КФО», «КФО Общий»,
+«Подтверждение КФО клиента» SHALL быть недоступны с подсказкой «Будет в следующих срезах». При смене `version_id` всё
+состояние страницы (карточка версии, данные, фильтры) SHALL создаваться заново.
+
+#### Scenario: Переход на шахматку
+- **WHEN** открыта сводная ОСВ версии по адресу `…/versions/{id}/osv` и нажата вкладка «Шахматка»
+- **THEN** адрес становится `…/versions/{id}/chess`, шапка версии остаётся, под вкладками показана шахматка
+
+#### Scenario: Прежний адрес сводной ОСВ
+- **WHEN** открыт адрес `…/versions/{id}/osv`
+- **THEN** показана сводная ОСВ, вкладка «ОСВ» активна
+
+#### Scenario: Неизвестная вкладка
+- **WHEN** открыт адрес `…/versions/{id}/abc`
+- **THEN** адрес заменяется на `…/versions/{id}/osv`, показана сводная ОСВ; кнопка «Назад» браузера не возвращает на `…/abc`
+
+### Requirement: Таблица шахматки ЮЛ × ЮЛ
+Экран SHALL загружать матрицу при открытии вкладки (`GET /consolidations/versions/{id}/intercompany-matrix/all`) и показывать
+таблицу: первая колонка «ДБ / КТ» (закреплена слева) — компании периметра в порядке ответа (`companies[]`), остальные колонки —
+те же компании. В ячейке пары (строка A, колонка Б) SHALL быть три числа друг под другом: сумма `amount_a`, сумма `amount_b`
+и расхождение `variance_amount` в скобках; расхождение выделяется красным, если `exceeds_threshold = true` или
+`one_sided = true` (пара без встречной стороны; правило подсветки — как в API и xlsx, AURORA-1651). Если у пары есть
+строки ОСВ без сопоставленного счёта (`skipped_osv_entry_count > 0`), рядом с первым числом SHALL показываться значок
+предупреждения с подсказкой «Строк ОСВ без сопоставленного счёта: N; в суммы не вошли». Если ячейки пары в ответе нет
+(в том числе на диагонали), SHALL показываться прочерк «—» на приглушённом фоне. Ниже таблицы закреплена строка «Итого»: до
+появления итогов в API (AURORA-1756, AURORA-1757) она временно считается на клиенте точным сложением десятичных строк — сумма
+`amount_a` по колонке видимых ячеек; итог, который не удалось сложить, показывается прочерком «—», а не нулём. Пока идёт
+загрузка, таблица показывает индикатор загрузки; ошибка API показывается уведомлением с текстом сервера. Панель фильтров
+показывает год и квартал версии неактивными полями «Год» и «Период» (например, `2024` и `К1`): версия задаётся адресом.
+
+#### Scenario: Ячейка с расхождением выше порога
+- **WHEN** для пары `(A, Б)` сервер вернул `exceeds_threshold = true`
+- **THEN** третье число ячейки (расхождение) показано красным, первые два — обычным цветом
+
+#### Scenario: Пара без встречной стороны
+- **WHEN** для пары `(A, Б)` сервер вернул `amount_b = 0`, `one_sided = true`, `exceeds_threshold = false`
+- **THEN** третье число ячейки (расхождение) показано красным
+
+#### Scenario: Пары нет в ответе
+- **WHEN** в `cells[]` нет ячейки для пары компаний
+- **THEN** на их пересечении показан прочерк «—»
+
+#### Scenario: Пропущенные строки ОСВ
+- **WHEN** у ячейки `skipped_osv_entry_count = 2`
+- **THEN** у первого числа ячейки виден значок предупреждения, подсказка — «Строк ОСВ без сопоставленного счёта: 2; в суммы не вошли»
+
+### Requirement: Результирующая таблица ВГО
+Под матрицей экран SHALL показывать результирующую таблицу ВГО, загруженную при открытии вкладки
+(`GET /consolidations/versions/{id}/intercompany-matrix/result-table`): первая колонка «Статья» — статьи ответа (`articles[]`,
+`name`) в порядке ответа, далее колонки компаний (`companies[]`) с суммой статьи по компании (`amounts[]` по `company_id`) и
+колонка «Итого» со значением `group_total` из ответа; пустая сумма — прочерк «—». Ниже таблицы закреплена строка «Итого»: до
+появления итогов в API (AURORA-1756, AURORA-1757) она временно считается на клиенте точным сложением сумм всех статей по
+колонке (итог, который не удалось сложить, — «—»). У таблицы свои выбор единиц и знаков (по правилам требования «Единицы и знаки», независимо от
+матрицы), кнопка «Экспорт» и кнопка «На весь экран». Пока идёт загрузка, таблица показывает индикатор; ошибка API (в том числе
+422 `RESULT_ACCOUNTS_MISSING`) показывается уведомлением с текстом сервера.
+
+#### Scenario: Колонка «Итого» из ответа
+- **WHEN** сервер вернул статью «Дебиторская задолженность» с `group_total = "1296510.00"`, выбраны «тыс. руб.» и 2 знака
+- **THEN** в колонке «Итого» этой статьи показано `1 296,51`
+
+#### Scenario: Нет обязательного счёта
+- **WHEN** сервер ответил 422 `RESULT_ACCOUNTS_MISSING`
+- **THEN** показано уведомление с текстом сервера, матрица шахматки при этом показывается как обычно
+
+### Requirement: Единицы и знаки
+Экран SHALL позволять выбрать единицы суммы — «руб.», «тыс. руб.» (по умолчанию), «млн. руб.», «млрд руб.» — и число знаков
+после запятой 0–3 (по умолчанию 2). Выбор применяется на клиенте к уже загруженным значениям (повторного запроса не делает):
+запятая сдвигается в десятичной строке на 3 / 6 / 9 разрядов, значение округляется до выбранного числа знаков (половина —
+от нуля) без перевода в число с плавающей точкой и показывается с разделителем разрядов и десятичной запятой. Значение,
+округлённое до нуля, показывается без минуса. Пустое значение — пустая ячейка. Значение, которое не удаётся прочитать как
+число, SHALL показываться как пришло — при любых единицах.
+
+#### Scenario: Смена единиц
+- **WHEN** выбраны «млн. руб.» и 1 знак, а сервер вернул `amount_a = "2346459.05"`
+- **THEN** в ячейке показано `2,3`
+
+#### Scenario: Точное округление
+- **WHEN** выбраны «тыс. руб.» и 2 знака, а сервер вернул `"1005"`
+- **THEN** показано `1,01` (не `1,00`, как дало бы округление числа с плавающей точкой)
+
+#### Scenario: Маленькая отрицательная сумма
+- **WHEN** выбраны «тыс. руб.» и 2 знака, а сервер вернул `"-4.00"`
+- **THEN** показано `0,00`, а не `-0,00`
+
+### Requirement: Фильтр компаний
+Выбор «Компании» (множественный, пустой выбор — «Все компании») SHALL ограничивать матрицу на клиенте: остаются строки и
+колонки выбранных компаний и ячейки, у которых обе компании выбраны. Запрос на сервер при этом не отправляется. Список
+вариантов — компании последнего загруженного ответа. Выпадающее окно выбора — дерево с поиском по названию, кнопками
+применения и сброса.
+
+#### Scenario: Две компании из периметра
+- **WHEN** выбраны две компании из пяти
+- **THEN** матрица содержит две строки и две колонки, «Итого» считается по оставшимся ячейкам
+
+### Requirement: Фильтр счетов Дт и Кт и режимы расчёта
+Экран SHALL загружать план счетов `GET /directories/accounts` один раз при открытии и строить из него два дерева по иерархии
+`parent_id`: дебетовые счета (`is_debit`) и кредитовые (`is_credit`); подпись узла — «код - название», в том числе у выбранных
+дочерних счетов в поле выбора. Выбор счетов Дт и Кт — множественный, пустой выбор — «Все счета».
+
+Отметки в дереве SHALL считаться по полному плану счетов: родительский счёт отмечен тогда и только тогда, когда отмечены все
+его потомки в полном дереве. Поиск по дереву скрывает не совпавшие узлы, но не меняет этого правила: отметка единственного
+видимого при поиске субсчёта не отмечает его родителя, а снятие отметки с субсчёта снимает её и с родителя. Скрытые поиском
+отметки при отметке видимых узлов сохраняются.
+
+Режим расчёта определяется выбором:
+- ничего не выбрано ни в Дт, ни в Кт — SHALL выполняться `GET …/intercompany-matrix/all` (режим `all`);
+- выбраны счета и в Дт, и в Кт — SHALL выполняться `POST …/intercompany-matrix/manual` с `{debit_account_ids,
+ credit_account_ids}` (режим `manual`);
+- выбрана только одна сторона — запрос SHALL NOT отправляться, таблица матрицы SHALL быть пустой и вместо данных показывать
+ текст «Для расчёта по счетам выберите счета и в Дт, и в Кт»; тот же текст SHALL показываться у фильтров Дт/Кт (не
+ всплывающим уведомлением) и только в этом состоянии; кнопка «Экспорт» матрицы SHALL быть недоступна.
+
+Каждое изменение выбора счетов, приводящее к режиму `all` или `manual`, перезапрашивает матрицу; на экране остаётся ответ на
+последний запрос, индикатор загрузки снимается и при переходе в состояние «выбрана одна сторона». Кнопка «Сбросить фильтры»
+появляется, когда выбрано что-либо в компаниях, Дт или Кт, и очищает все три выбора. Выбор единиц и знаков кнопкой не
+сбрасывается. Ошибка загрузки плана счетов показывается уведомлением.
+
+#### Scenario: Обе стороны выбраны
+- **WHEN** выбран счёт 60.01 в Дт и счёт 62.01 в Кт
+- **THEN** отправляется `POST …/intercompany-matrix/manual` с идентификаторами этих счетов и таблица показывает ответ
+
+#### Scenario: Выбрана одна сторона
+- **WHEN** выбран счёт только в Дт
+- **THEN** запрос матрицы не отправляется, таблица пуста и показывает «Для расчёта по счетам выберите счета и в Дт, и в Кт»,
+ тот же текст виден у фильтров, «Экспорт» матрицы недоступен, выбор Дт остаётся в фильтре
+
+#### Scenario: Выбрана одна сторона во время загрузки
+- **WHEN** выбраны Дт и Кт, запрос `manual` ещё выполняется, и пользователь очищает Кт
+- **THEN** индикатор загрузки снят, ответ прежнего запроса на экран не выводится, таблица в состоянии «выбрана одна сторона»
+
+#### Scenario: Поиск и отметка субсчёта
+- **WHEN** в дереве Дт введён поиск, видна ветка 60 с единственным субсчётом 60.02, и отмечен 60.02
+- **THEN** выбранным остаётся только 60.02; после сброса поиска счёт 60 и его прочие субсчета не отмечены
+
+#### Scenario: Снятие субсчёта отмеченного родителя
+- **WHEN** отмечен счёт 60 (отмечены 60, 60.01, 60.02) и снята отметка с 60.01
+- **THEN** выбранным остаётся 60.02, счёт 60 не отмечен
+
+#### Scenario: Сброс
+- **WHEN** выбраны компании и счета, нажато «Сбросить фильтры»
+- **THEN** все три выбора пусты, матрица перезапрошена в режиме `all`, кнопка исчезла
+
+### Requirement: Экспорт в XLSX
+Кнопка «Экспорт» матрицы SHALL скачивать XLSX в том же режиме, что и таблица: в режиме `manual` —
+`POST …/intercompany-matrix/manual/export` с телом `{debit_account_ids, credit_account_ids}`, в режиме `all` —
+`GET …/intercompany-matrix/all/export`; при выбранной одной стороне кнопка недоступна. Кнопка «Экспорт» результирующей таблицы
+SHALL скачивать `GET …/intercompany-matrix/result-table/export`. Файл сохраняется под именем из `Content-Disposition`, при его
+отсутствии — `Шахматка_<ГК>_<год>_<квартал>.xlsx` и `Результирующая_таблица_ВГО_<ГК>_<год>_<квартал>.xlsx` соответственно.
+Пока идёт выгрузка, в состоянии загрузки только нажатая кнопка; ошибка API показывается уведомлением. Фильтр компаний, единицы и
+знаки в запрос экспорта не передаются.
+
+#### Scenario: Экспорт по всем счетам
+- **WHEN** счета не выбраны и нажато «Экспорт» матрицы
+- **THEN** отправлен `GET …/intercompany-matrix/all/export`, браузер сохраняет файл `intercompany-matrix_….xlsx`
+
+#### Scenario: Экспорт результирующей таблицы
+- **WHEN** нажато «Экспорт» результирующей таблицы
+- **THEN** отправлен `GET …/intercompany-matrix/result-table/export`, кнопка «Экспорт» матрицы не переходит в состояние загрузки
+
+### Requirement: Полноэкранный режим
+Кнопка «На весь экран» матрицы и результирующей таблицы SHALL переводить свой блок (панель фильтров и таблицу) в
+полноэкранный режим средствами Fullscreen API с белым фоном и таблицей на высоту окна; в нём кнопка меняется на «Свернуть».
+Выпадающие списки фильтров и подсказки SHALL оставаться видимыми в полноэкранном режиме. Выход клавишей Esc SHALL возвращать
+обычный вид и подпись кнопки. Отказ браузера войти в полноэкранный режим показывается уведомлением.
+
+#### Scenario: Вход и выход
+- **WHEN** нажато «На весь экран», затем Esc
+- **THEN** блок разворачивается на весь экран, затем возвращается в страницу, кнопка снова «На весь экран»
+
+#### Scenario: Фильтр в полноэкранном режиме
+- **WHEN** матрица в полноэкранном режиме и открыт выбор «Компании»
+- **THEN** выпадающее окно видно поверх полноэкранного блока
diff --git a/web/ARCHITECTURE.md b/web/ARCHITECTURE.md
index 1aa3bd0..12e7f29 100644
--- a/web/ARCHITECTURE.md
+++ b/web/ARCHITECTURE.md
@@ -35,7 +35,7 @@ SPA поверх API arm-core (`/api/v1`). Своих данных и бизне
| `../hooks/useBeforeUnload.ts` | — | предупреждение браузера о несохранённых правках в открытой модалке |
| `ConsoleLegalEntities/` | [34] | панель «Организации» выбранной версии и чек-лист |
| `NewConsoleModal/` (`CompanyOptionRow.tsx`) | [42] | «Новая консолидация»; строка ЮЛ с ИНН отдельной колонкой |
-| `StatusChangeModal/ChangeStatusModal.tsx` | [33-01] | «Изменить статус» (`POST …/status`); открывается из шапки `layouts/ConsolidatedOsvPage.tsx` |
+| `StatusChangeModal/ChangeStatusModal.tsx` | [33-01] | «Изменить статус» (`POST …/status`); открывается из шапки `layouts/ConsolidationReportPage.tsx` |
## Границы
- Сетевые запросы — только через `src/api/client.ts`; `fetch` напрямую не вызывается.
diff --git a/web/CLAUDE.md b/web/CLAUDE.md
index 6708827..8309ab3 100644
--- a/web/CLAUDE.md
+++ b/web/CLAUDE.md
@@ -49,7 +49,7 @@ arm-web перенесены в arm-core #150–#157 (arm-web#7 → #151), сп
- `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` — проверка ББ/ОФР/ОСВ
+ `/groups/:group_id/versions/:version_id/:tab` — страница отчёта с вкладками `osv`, `chess`), `src/widgets/` — блоки экранов (`DocumentReview` — проверка ББ/ОФР/ОСВ
со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer` — файлы ЮЛ, `ConsoleTable` — список консолидаций ГК и
«Взять в работу», `StatusChangeModal` — «Изменить статус» версии), `src/components/` — примитивы
(`GenericTable`, `TableTag`, `PageTitle`), `src/types/` — DTO API.
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 24faf7a..871c730 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -10,7 +10,6 @@ import {
AuditLogging,
CatalogueGroup,
CatalogueJur,
- ConsolidatedOsvPage,
ConsolidationPage,
DocStorage,
DocumentPage,
@@ -18,6 +17,7 @@ import {
Main,
Mono,
} from './layouts/';
+import { ConsolidationReportPage } from './layouts/ConsolidationReportPage';
import { store } from './store/store';
import { Sidebar } from './widgets/Sidebar/Sidebar';
@@ -37,7 +37,7 @@ function App() {
} />
} />
- } />
+ } />
} />
} />
diff --git a/web/src/api/client.ts b/web/src/api/client.ts
index e6522d2..2aa72d4 100644
--- a/web/src/api/client.ts
+++ b/web/src/api/client.ts
@@ -127,13 +127,19 @@ export async function apiFetch(path: string, init: RequestInit = {}): Promise
}
/** Бинарный ответ (скан, xlsx) с токеном и тем же разбором ошибок; имя — из Content-Disposition, если доступно. */
-export async function apiFetchBlob(path: string): Promise<{ blob: Blob; filename: string | null }> {
- const response = await send(path, {});
+export async function apiFetchBlob(
+ path: string,
+ init: RequestInit = {}, // Добавили init
+): Promise<{ blob: Blob; filename: string | null }> {
+ const response = await send(path, init);
const blob = await response.blob();
- return { blob, filename: filenameFromContentDisposition(response.headers.get('Content-Disposition')) };
+ return {
+ blob,
+ filename: filenameFromContentDisposition(response.headers.get('Content-Disposition')),
+ };
}
-function withJsonBody(method: string, body: unknown): RequestInit {
+export function withJsonBody(method: string, body: unknown): RequestInit {
return body === undefined ? { method } : { method, body: JSON.stringify(body) };
}
diff --git a/web/src/components/TreeFilterDropdown/TreeFilterDropdown.css b/web/src/components/TreeFilterDropdown/TreeFilterDropdown.css
new file mode 100644
index 0000000..adf8ee1
--- /dev/null
+++ b/web/src/components/TreeFilterDropdown/TreeFilterDropdown.css
@@ -0,0 +1,3 @@
+.filterTree .ant-tree-switcher-noop {
+ display: none !important;
+}
diff --git a/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx b/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx
new file mode 100644
index 0000000..5e5f680
--- /dev/null
+++ b/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx
@@ -0,0 +1,152 @@
+import { SearchOutlined } from '@ant-design/icons';
+import { Button, Input, Space, Tree } from 'antd';
+import { type ReactNode, useEffect, useMemo, useState } from 'react';
+import { toggleTreeCheck } from '../../utils/treeChecks';
+import './TreeFilterDropdown.css';
+
+export type OptionType = {
+ title: string;
+ key: string;
+ code?: string;
+ children?: OptionType[];
+};
+
+interface TreeFilterDropdownProps {
+ title: string;
+ placeholder?: string;
+ treeData: OptionType[];
+ selectedKeys: string[];
+ onChange: (keys: string[]) => void;
+ onClose: () => void;
+ warningBlock?: ReactNode;
+}
+
+export const TreeFilterDropdown: React.FC = ({
+ title,
+ placeholder = 'Введите значение',
+ treeData,
+ selectedKeys,
+ onChange,
+ onClose,
+ warningBlock,
+}) => {
+ const [query, setQuery] = useState('');
+
+ const [checkedKeys, setCheckedKeys] = useState(selectedKeys);
+ const [expandedKeys, setExpandedKeys] = useState([]);
+
+ const filteredTreeData = useMemo(() => {
+ if (!query) return treeData;
+
+ const filterNode = (node: OptionType): OptionType | null => {
+ const titleStr = String(node.title).toLowerCase();
+ const isMatch = titleStr.includes(query.toLowerCase());
+
+ let filteredChildren: OptionType[] = [];
+ if (node.children) {
+ filteredChildren = node.children.map(filterNode).filter((child): child is OptionType => child !== null);
+ }
+
+ if (isMatch || filteredChildren.length > 0) {
+ return {
+ ...node,
+ children: filteredChildren.length > 0 ? filteredChildren : undefined,
+ };
+ }
+ return null;
+ };
+
+ return treeData.map(filterNode).filter((node): node is OptionType => node !== null);
+ }, [treeData, query]);
+
+ useEffect(() => {
+ if (query) {
+ const getAllKeys = (nodes: OptionType[]): string[] => {
+ let keys: string[] = [];
+ nodes.forEach((node) => {
+ keys.push(node.key);
+ if (node.children) keys = [...keys, ...getAllKeys(node.children)];
+ });
+ return keys;
+ };
+ setExpandedKeys(getAllKeys(filteredTreeData));
+ }
+ }, [query, filteredTreeData]);
+
+ const getAllNodeKeys = (nodes: OptionType[]): string[] => {
+ let keys: string[] = [];
+ nodes.forEach((node) => {
+ keys.push(node.key);
+ if (node.children) keys = [...keys, ...getAllNodeKeys(node.children)];
+ });
+ return keys;
+ };
+
+ const allFilteredKeys = getAllNodeKeys(filteredTreeData);
+ const visibleCheckedKeys = checkedKeys.filter((key) => allFilteredKeys.includes(key));
+
+ const handleSelectAll = () => {
+ setCheckedKeys(getAllNodeKeys(treeData));
+ };
+
+ const handleClear = () => {
+ setCheckedKeys([]);
+ };
+
+ const handleCancel = () => {
+ setCheckedKeys(selectedKeys);
+ onClose();
+ };
+
+ function checkHandler(keys: string[], info: { node: OptionType; checked: boolean }) {
+ setCheckedKeys(toggleTreeCheck(treeData, checkedKeys, keys, info.node.key, info.checked));
+ }
+ useEffect(() => {
+ setCheckedKeys(selectedKeys);
+ }, [selectedKeys]);
+
+ return (
+
- Требуется указать соответствия
- Для перехода к следующему шагу сопоставьте все внешние организации без ИНН. Осталось: {unmapped}.
- {/* макет оставляет кнопку как есть, снимать фильтр — воронкой ИНН или «Сбросить фильтры» (панель — позже) */}
- }
- onClick={() => osv.patchFilters({ unmapped: !onlyUnmapped })}>
- {onlyUnmapped ? 'Показать все строки' : 'Показать ЮЛ без ИНН'}
-
-
+ Требуется указать соответствия
+ Для перехода к следующему шагу сопоставьте все внешние организации без ИНН. Осталось: {unmapped}.
+ {/* макет оставляет кнопку как есть, снимать фильтр — воронкой ИНН или «Сбросить фильтры» (панель — позже) */}
+ }
+ onClick={() => osv.patchFilters({ unmapped: !onlyUnmapped })}>
+ {onlyUnmapped ? 'Показать все строки' : 'Показать ЮЛ без ИНН'}
+
+