diff --git a/openspec/changes/intercompany-matrix-totals-ui/design.md b/openspec/changes/intercompany-matrix-totals-ui/design.md new file mode 100644 index 0000000..3b7ccc1 --- /dev/null +++ b/openspec/changes/intercompany-matrix-totals-ui/design.md @@ -0,0 +1,17 @@ +# Design + +## Решения + +- **Варианты фильтра «Компании».** После перехода фильтра на сервер ответ с выбором содержит только выбранные ЮЛ, поэтому + варианты берутся из последнего ответа без выбора (`isWholePerimeter`). Первый запрос вкладки всегда без выбора. +- **Итоги не пересчитываются на клиенте.** Только форматирование `chessTableCellFormatValue`; нет итога — «—». +- **Одна сторона Дт/Кт.** Бэк теперь умеет «пустая сторона = все балансовые счета», но поведение экрана (подсказка, запрос + не шлём) не меняем — это AURORA-1799. + +## Decisions made without discussion + +| Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---| +| 2026-11-06 | Задача ведётся как bounded, но с OpenSpec change для спеки экрана | спека — источник истины, правка `specs/` напрямую — то, что сами отметили в ревью #218 | нет | +| 2026-11-06 | Колонка результирующей таблицы переименована «Итого» → «Итого по группе» | так в DOC-221277 и в xlsx; рядом исчезла строка «Итого», два разных «Итого» больше не путаются | косметика | +| 2026-11-06 | Каждая отметка в фильтре «Компании» перезапрашивает матрицу без задержки | так уже работают фильтры Дт/Кт; гонки гасит `isActual` | лишние запросы при быстром выборе — можно добавить задержку | diff --git a/openspec/changes/intercompany-matrix-totals-ui/proposal.md b/openspec/changes/intercompany-matrix-totals-ui/proposal.md new file mode 100644 index 0000000..3e737e4 --- /dev/null +++ b/openspec/changes/intercompany-matrix-totals-ui/proposal.md @@ -0,0 +1,26 @@ +# Proposal + +## Why + +Итоги шахматки на экране считались на клиенте (PR #158), фильтр «Компании» скрывал строки без запроса. Бэк (AURORA-1756, +PR #218, change `intercompany-matrix-totals`) теперь отдаёт `total_debit` / `total_credit`, принимает `legal_entity_ids` и +заменил `GET …/all` и `POST …/manual` единым `POST …/intercompany-matrix`. AURORA-1757, issue arm-core #224. + +## What Changes + +- Клиент — единый `POST …/intercompany-matrix` и `…/export`. +- Матрица: колонка «Итого Дт» справа и строка «Итого Кт» снизу из ответа API; клиентское сложение убрано. +- Фильтр «Компании» уходит в запрос; варианты — весь периметр из ответа без выбора компаний. +- Выгрузка матрицы — с тем же выбором компаний. +- Результирующая таблица: строки «Итого» снизу нет, колонка — «Итого по группе». + +## Capabilities + +### Modified Capabilities + +- `intercompany-matrix-ui` + +## Impact + +`web/src`: `services/api/intercompanyMatrix.ts`, `types/api.ts`, `utils/chessMatrixRequest.ts`, `widgets/ConsolidationChess/*`. +Сливается вместе с #218. Состояние «выбрана одна сторона» не меняется (AURORA-1799). diff --git a/openspec/changes/intercompany-matrix-totals-ui/specs/intercompany-matrix-ui/spec.md b/openspec/changes/intercompany-matrix-totals-ui/specs/intercompany-matrix-ui/spec.md new file mode 100644 index 0000000..11d34bd --- /dev/null +++ b/openspec/changes/intercompany-matrix-totals-ui/specs/intercompany-matrix-ui/spec.md @@ -0,0 +1,129 @@ +## MODIFIED Requirements + +### Requirement: Таблица шахматки ЮЛ × ЮЛ +Экран SHALL загружать матрицу при открытии вкладки (`POST /consolidations/versions/{id}/intercompany-matrix`, тело — по требованию «Фильтр счетов Дт и Кт и режимы расчёта») и показывать +таблицу: первая колонка «ДБ / КТ» (закреплена слева) — компании периметра в порядке ответа (`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 показываться прочерк «—» на приглушённом фоне. Справа закреплена колонка «Итого Дт» — +`total_debit` ответа для компании строки; ниже таблицы закреплена строка «Итого Кт» — `total_credit` для компании колонки; +ячейка на их пересечении пустая. Итоги считает сервер (DOC-221277 «Итоги шахматки», AURORA-1756), клиент их только форматирует +(«Единицы и знаки»); отсутствующий в ответе итог — прочерк «—». Пока идёт +загрузка, таблица показывает индикатор загрузки; ошибка 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; в суммы не вошли» + +#### Scenario: Итоги из ответа +- **WHEN** сервер вернул `total_debit[A] = "120000.00"` и `total_credit[Б] = "75000.00"`, выбраны «тыс. руб.» и 2 знака +- **THEN** в колонке «Итого Дт» строки A показано `120,00`, в строке «Итого Кт» под колонкой Б — `75,00`, пересечение итогов пустое + +### Requirement: Результирующая таблица ВГО +Под матрицей экран SHALL показывать результирующую таблицу ВГО, загруженную при открытии вкладки +(`GET /consolidations/versions/{id}/intercompany-matrix/result-table`): первая колонка «Статья» — статьи ответа (`articles[]`, +`name`) в порядке ответа, далее колонки компаний (`companies[]`) с суммой статьи по компании (`amounts[]` по `company_id`) и +колонка «Итого по группе» со значением `group_total` из ответа; пустая сумма — прочерк «—». Строки итога по всем статьям под +таблицей нет: суммы разных статей не складываются (аналитик, 03.11.2026). У таблицы свои выбор единиц и знаков (по правилам требования «Единицы и знаки», независимо от +матрицы), кнопка «Экспорт» и кнопка «На весь экран». Пока идёт загрузка, таблица показывает индикатор; ошибка 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 передаваться в запрос матрицы +`legal_entity_ids`: сервер возвращает только выбранные компании, ячейки между ними и итоги «Итого Дт» / «Итого Кт» по ним. +Каждое изменение выбора перезапрашивает матрицу (в состоянии «выбрана одна сторона» — нет). Список вариантов — компании +последнего ответа, полученного без выбора компаний (весь периметр версии); ответы с выбором его не меняют. Выпадающее окно выбора — дерево с поиском по названию, кнопками +применения и сброса. + +#### Scenario: Две компании из периметра +- **WHEN** выбраны две компании из пяти +- **THEN** запрос содержит `legal_entity_ids` этих двух компаний, матрица содержит две строки и две колонки, итоги — из ответа, в списке вариантов по-прежнему пять компаний + +### Requirement: Фильтр счетов Дт и Кт и режимы расчёта +Экран SHALL загружать план счетов `GET /directories/accounts` один раз при открытии и строить из него два дерева по иерархии +`parent_id`: дебетовые счета (`is_debit`) и кредитовые (`is_credit`); подпись узла — «код - название», в том числе у выбранных +дочерних счетов в поле выбора. Выбор счетов Дт и Кт — множественный, пустой выбор — «Все счета». + +Отметки в дереве SHALL считаться по полному плану счетов: родительский счёт отмечен тогда и только тогда, когда отмечены все +его потомки в полном дереве. Поиск по дереву скрывает не совпавшие узлы, но не меняет этого правила: отметка единственного +видимого при поиске субсчёта не отмечает его родителя, а снятие отметки с субсчёта снимает её и с родителя. Скрытые поиском +отметки при отметке видимых узлов сохраняются. + +Режим расчёта определяется выбором: +- ничего не выбрано ни в Дт, ни в Кт — SHALL выполняться `POST …/intercompany-matrix` с пустыми `debit_account_ids` и + `credit_account_ids` (режим `all`); +- выбраны счета и в Дт, и в Кт — SHALL выполняться `POST …/intercompany-matrix` с выбранными `debit_account_ids` и + `credit_account_ids` (режим `manual`); +- выбрана только одна сторона — запрос SHALL NOT отправляться, таблица матрицы SHALL быть пустой и вместо данных показывать + текст «Для расчёта по счетам выберите счета и в Дт, и в Кт»; тот же текст SHALL показываться у фильтров Дт/Кт (не + всплывающим уведомлением) и только в этом состоянии; кнопка «Экспорт» матрицы SHALL быть недоступна. + +В тело всегда входит `legal_entity_ids` (требование «Фильтр компаний»). Каждое изменение выбора счетов, приводящее к режиму `all` или `manual`, перезапрашивает матрицу; на экране остаётся ответ на +последний запрос, индикатор загрузки снимается и при переходе в состояние «выбрана одна сторона». Кнопка «Сбросить фильтры» +появляется, когда выбрано что-либо в компаниях, Дт или Кт, и очищает все три выбора. Выбор единиц и знаков кнопкой не +сбрасывается. Ошибка загрузки плана счетов показывается уведомлением. + +#### Scenario: Обе стороны выбраны +- **WHEN** выбран счёт 60.01 в Дт и счёт 62.01 в Кт +- **THEN** отправляется `POST …/intercompany-matrix` с идентификаторами этих счетов и таблица показывает ответ + +#### 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 в том же режиме, что и таблица: `POST …/intercompany-matrix/export` с тем же +телом, что запрос таблицы (`debit_account_ids`, `credit_account_ids`, `legal_entity_ids`), — итоги в файле совпадают с экраном; при выбранной одной стороне кнопка недоступна. Кнопка «Экспорт» результирующей таблицы +SHALL скачивать `GET …/intercompany-matrix/result-table/export`. Файл сохраняется под именем из `Content-Disposition`, при его +отсутствии — `Шахматка_<ГК>_<год>_<квартал>.xlsx` и `Результирующая_таблица_ВГО_<ГК>_<год>_<квартал>.xlsx` соответственно. +Пока идёт выгрузка, в состоянии загрузки только нажатая кнопка; ошибка API показывается уведомлением. Единицы и знаки в запрос экспорта +не передаются. + +#### Scenario: Экспорт по всем счетам +- **WHEN** счета не выбраны и нажато «Экспорт» матрицы +- **THEN** отправлен `POST …/intercompany-matrix/export` с пустыми списками, браузер сохраняет файл `intercompany-matrix_….xlsx` + +#### Scenario: Экспорт результирующей таблицы +- **WHEN** нажато «Экспорт» результирующей таблицы +- **THEN** отправлен `GET …/intercompany-matrix/result-table/export`, кнопка «Экспорт» матрицы не переходит в состояние загрузки diff --git a/openspec/changes/intercompany-matrix-totals-ui/tasks.md b/openspec/changes/intercompany-matrix-totals-ui/tasks.md new file mode 100644 index 0000000..f14caf9 --- /dev/null +++ b/openspec/changes/intercompany-matrix-totals-ui/tasks.md @@ -0,0 +1,8 @@ +# Tasks + +- [x] 1.1 Клиент `fetchIntercompanyMatrix` / `exportIntercompanyMatrix`, тип `IntercompanyMatrixRequest` +- [x] 1.2 `chessMatrixRequest` / `isWholePerimeter` + тесты +- [x] 1.3 Матрица: «Итого Дт» / «Итого Кт» из ответа +- [x] 1.4 Фильтр «Компании» в запросе, варианты из периметра; выгрузка с выбором компаний +- [x] 1.5 Результирующая таблица без строки «Итого» +- [ ] 1.6 Сценарий на стенде (Playwright) и сверка с макетом 31-01 / 31-01-01 diff --git a/web/src/services/api/intercompanyMatrix.ts b/web/src/services/api/intercompanyMatrix.ts index db626df..0dbd6b9 100644 --- a/web/src/services/api/intercompanyMatrix.ts +++ b/web/src/services/api/intercompanyMatrix.ts @@ -1,17 +1,13 @@ import { api, apiFetchBlob, withJsonBody } from '../../api/client'; -import type { ChessManualRequest, IntercompanyMatrixResponse, IntercompanyMatrixResultResponse } from '../../types/api'; +import type { IntercompanyMatrixRequest, IntercompanyMatrixResponse, IntercompanyMatrixResultResponse } from '../../types/api'; const base = (versionId: string) => `/consolidations/versions/${versionId}/intercompany-matrix`; -export const fetchChessConsolidations = (versionId: string) => api.get(`${base(versionId)}/all`); +export const fetchIntercompanyMatrix = (versionId: string, data: IntercompanyMatrixRequest) => + api.post(base(versionId), data); -export const fetchChessConsolidationsManual = (versionId: string, data: ChessManualRequest) => - api.post(`${base(versionId)}/manual`, data); - -export const exportChessTableAll = (versionId: string) => apiFetchBlob(`${base(versionId)}/all/export`); - -export const exportChessTableManual = (versionId: string, data: ChessManualRequest) => - apiFetchBlob(`${base(versionId)}/manual/export`, withJsonBody('POST', data)); +export const exportIntercompanyMatrix = (versionId: string, data: IntercompanyMatrixRequest) => + apiFetchBlob(`${base(versionId)}/export`, withJsonBody('POST', data)); export const fetchChessConsolidationsResult = (versionId: string) => api.get(`${base(versionId)}/result-table`); diff --git a/web/src/types/api.ts b/web/src/types/api.ts index 3293854..6889769 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -455,11 +455,17 @@ export interface MatrixCell { export interface IntercompanyMatrixResponse { companies: MatrixCompany[]; cells: MatrixCell[]; + /** «Итого Дт»: сумма `amount_a` строки компании, ключ — ID ЮЛ. */ + total_debit: Record; + /** «Итого Кт»: сумма `amount_b` столбца компании, ключ — ID ЮЛ. */ + total_credit: Record; } -export type ChessManualRequest = { +/** Тело `POST …/intercompany-matrix`: пустой список — все балансовые счета стороны / весь периметр. */ +export type IntercompanyMatrixRequest = { debit_account_ids: string[]; credit_account_ids: string[]; + legal_entity_ids: string[]; }; /** Результирующая таблица ВГО (`IntercompanyResultTableOut`): статьи §6 по компаниям. */ diff --git a/web/src/utils/chessMatrixRequest.test.ts b/web/src/utils/chessMatrixRequest.test.ts new file mode 100644 index 0000000..fc84779 --- /dev/null +++ b/web/src/utils/chessMatrixRequest.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { chessMatrixRequest, isWholePerimeter } from './chessMatrixRequest'; + +describe('chessMatrixRequest', () => { + it('пустые фильтры — пустые списки: все балансовые счета и весь периметр', () => { + expect(chessMatrixRequest([], [], [])).toEqual({ debit_account_ids: [], credit_account_ids: [], legal_entity_ids: [] }); + }); + it('выбранные счета и компании уходят в запрос как есть', () => { + expect(chessMatrixRequest(['d1'], ['c1', 'c2'], ['le1'])).toEqual({ + debit_account_ids: ['d1'], + credit_account_ids: ['c1', 'c2'], + legal_entity_ids: ['le1'], + }); + }); + it('списки копируются: правка фильтра после отправки не меняет тело', () => { + const companies = ['le1']; + const body = chessMatrixRequest([], [], companies); + companies.push('le2'); + expect(body.legal_entity_ids).toEqual(['le1']); + }); +}); + +describe('isWholePerimeter', () => { + it('без выбора компаний ответ содержит весь периметр — из него строятся варианты фильтра', () => { + expect(isWholePerimeter(chessMatrixRequest(['d1'], ['c1'], []))).toBe(true); + }); + it('с выбором компаний ответ сужен — варианты фильтра не трогаем', () => { + expect(isWholePerimeter(chessMatrixRequest([], [], ['le1']))).toBe(false); + }); +}); diff --git a/web/src/utils/chessMatrixRequest.ts b/web/src/utils/chessMatrixRequest.ts new file mode 100644 index 0000000..aedaab1 --- /dev/null +++ b/web/src/utils/chessMatrixRequest.ts @@ -0,0 +1,19 @@ +import type { IntercompanyMatrixRequest } from '../types/api'; + +/** + * Тело `POST …/intercompany-matrix` и `…/export` по фильтрам экрана (спека consolidated-osv, AURORA-1756/1757). + * Пустой список стороны — все балансовые счета этой стороны, пустой `legal_entity_ids` — весь периметр версии. + * Компании отбирает сервер, чтобы «Итого Дт» и «Итого Кт» считались по выбранным ЮЛ. + */ +export const chessMatrixRequest = ( + debit: readonly string[], + credit: readonly string[], + companies: readonly string[], +): IntercompanyMatrixRequest => ({ + debit_account_ids: [...debit], + credit_account_ids: [...credit], + legal_entity_ids: [...companies], +}); + +/** Ответ на запрос без выбора компаний содержит весь периметр — из него строятся варианты фильтра «Компании». */ +export const isWholePerimeter = (request: IntercompanyMatrixRequest): boolean => request.legal_entity_ids.length === 0; diff --git a/web/src/widgets/ConsolidationChess/ConsolidationChessContent/ConsolidatedChessContent.tsx b/web/src/widgets/ConsolidationChess/ConsolidationChessContent/ConsolidatedChessContent.tsx index a64b8c3..cc41295 100644 --- a/web/src/widgets/ConsolidationChess/ConsolidationChessContent/ConsolidatedChessContent.tsx +++ b/web/src/widgets/ConsolidationChess/ConsolidationChessContent/ConsolidatedChessContent.tsx @@ -1,19 +1,23 @@ import { DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons'; import { Button, ConfigProvider } from 'antd'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { ApiError } from '../../../api/client'; import { exportChessConsolidationsResult, - exportChessTableAll, - exportChessTableManual, - fetchChessConsolidations, - fetchChessConsolidationsManual, + exportIntercompanyMatrix, fetchChessConsolidationsResult, + fetchIntercompanyMatrix, } from '../../../services/api/intercompanyMatrix'; -import type { IntercompanyMatrixResponse, IntercompanyMatrixResultResponse } from '../../../types/api'; +import type { + IntercompanyMatrixRequest, + IntercompanyMatrixResponse, + IntercompanyMatrixResultResponse, + MatrixCompany, +} from '../../../types/api'; import type { VersionCard } from '../../../types/consolidations/version_members'; import { chessMatrixMode } from '../../../utils/chessMatrixMode'; +import { chessMatrixRequest, isWholePerimeter } from '../../../utils/chessMatrixRequest'; import { saveBlob, xlsxFilename } from '../../../utils/download'; import { useNotificationApi } from '../../../utils/useNotification'; import { ConsolidationChessFormatTableFilter } from '../ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter'; @@ -30,6 +34,8 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => { const [matrixExportLoading, setMatrixExportLoading] = useState(false); const [resExportLoading, setResExportLoading] = useState(false); const [companies, setCompanies] = useState([]); + // варианты фильтра «Компании» — весь периметр версии; при выборе компаний ответ сужен и их не меняет + const [perimeter, setPerimeter] = useState([]); const [decimals, setDecimals] = useState(2); const [units, setUnits] = useState('thousand'); const [debit, setDebit] = useState([]); @@ -77,17 +83,15 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => { const isHalfFiltered = matrixMode === 'half'; const loadData = useCallback( - async (vId: string, dIds: string[], cIds: string[], isActual: () => boolean) => { + async (vId: string, request: IntercompanyMatrixRequest, isActual: () => boolean) => { setLoading(true); try { - let res: IntercompanyMatrixResponse; - if (chessMatrixMode(dIds, cIds) === 'manual') { - res = await fetchChessConsolidationsManual(vId, { debit_account_ids: dIds, credit_account_ids: cIds }); - } else { - res = await fetchChessConsolidations(vId); - } + const res = await fetchIntercompanyMatrix(vId, request); if (isActual()) { setData(res); + if (isWholePerimeter(request)) { + setPerimeter(res.companies); + } } } catch (e) { if (isActual() && e instanceof ApiError) { @@ -126,13 +130,9 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => { } setMatrixExportLoading(true); try { - if (matrixMode === 'manual') { - const { blob, filename } = await exportChessTableManual(version_id, { debit_account_ids: debit, credit_account_ids: credit }); - saveBlob(blob, filename ?? xlsxFilename('Шахматка', versionInfo)); - } else { - const { blob, filename } = await exportChessTableAll(version_id); - saveBlob(blob, filename ?? xlsxFilename('Шахматка', versionInfo)); - } + // выгрузка по тем же счетам и компаниям, что на экране: итоги в xlsx совпадают с таблицей + const { blob, filename } = await exportIntercompanyMatrix(version_id, chessMatrixRequest(debit, credit, companies)); + saveBlob(blob, filename ?? xlsxFilename('Шахматка', versionInfo)); } catch (e) { if (e instanceof ApiError) { notification.error({ description: e.message }); @@ -158,16 +158,6 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => { } }; - const filteredData = useMemo(() => { - if (!data) return null; - if (companies.length === 0) return data; - - const filteredCompanies = data.companies.filter((c) => companies.includes(c.id)); - const filteredCells = data.cells.filter((c) => companies.includes(c.company_a_id) && companies.includes(c.company_b_id)); - - return { companies: filteredCompanies, cells: filteredCells }; - }, [data, companies]); - useEffect(() => { // выбрана только одна сторона Дт/Кт — запрос не шлём, таблица показывает подсказку if (!version_id || isHalfFiltered) return; @@ -175,13 +165,13 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => { let isMounted = true; const isActual = () => isMounted; - loadData(version_id, debit, credit, isActual); + loadData(version_id, chessMatrixRequest(debit, credit, companies), isActual); return () => { isMounted = false; setLoading(false); }; - }, [version_id, debit, credit, isHalfFiltered, loadData]); + }, [version_id, debit, credit, companies, isHalfFiltered, loadData]); useEffect(() => { if (!version_id) return; @@ -211,7 +201,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => { flexDirection: 'column', }}> { onDebitChange={setDebit} credit={credit} onCreditChange={setCredit} - allCompanies={data?.companies || []} + allCompanies={perimeter} year={card?.year} quarter={card?.quarter} /> diff --git a/web/src/widgets/ConsolidationChess/ConsolidationChessResTable/ConsolidationChessResTable.tsx b/web/src/widgets/ConsolidationChess/ConsolidationChessResTable/ConsolidationChessResTable.tsx index e7cf108..f55cc71 100644 --- a/web/src/widgets/ConsolidationChess/ConsolidationChessResTable/ConsolidationChessResTable.tsx +++ b/web/src/widgets/ConsolidationChess/ConsolidationChessResTable/ConsolidationChessResTable.tsx @@ -5,7 +5,6 @@ import { useMemo } from 'react'; import type { IntercompanyMatrixResultResponse, MatrixResCell } from '../../../types/api'; import './ConsolidatedChessResTable.css'; import { chessTableCellFormatValue } from '../../../utils/chessTableCellFormatValue'; -import { sumAmounts } from '../../../utils/money'; const { Text } = Typography; @@ -66,7 +65,7 @@ export const ConsolidationChessResTable: React.FC = })); const totalColumn: ColumnsType[number] = { - title: 'Итого', + title: 'Итого по группе', dataIndex: 'group_total', key: 'group_total', width: 150, @@ -91,44 +90,6 @@ export const ConsolidationChessResTable: React.FC = size='medium' bordered title={() => toolbar} - summary={() => { - if (!data?.articles) return null; - - return ( - - - -
- Итого -
-
- - {data.companies.map((company, index) => { - const amountsToSum: string[] = []; - data.articles.forEach((article) => { - const amount = matrixData[article.code]?.[company.id]; - if (amount) amountsToSum.push(amount); - }); - - // сумму, которую не удалось сложить, не выдумываем нулём — ячейка покажет «—» - const colTotal = sumAmounts(amountsToSum) ?? ''; - - return ( - - {chessTableCellFormatValue(colTotal, units, decimals) || DASH} - - ); - })} - - - - {chessTableCellFormatValue(sumAmounts(data.articles.map((a) => a.group_total)) ?? '', units, decimals) || DASH} - - -
-
- ); - }} /> ); }; diff --git a/web/src/widgets/ConsolidationChess/ConsolidationChessTable/ConsolidationChessTable.tsx b/web/src/widgets/ConsolidationChess/ConsolidationChessTable/ConsolidationChessTable.tsx index b657f42..208ca49 100644 --- a/web/src/widgets/ConsolidationChess/ConsolidationChessTable/ConsolidationChessTable.tsx +++ b/web/src/widgets/ConsolidationChess/ConsolidationChessTable/ConsolidationChessTable.tsx @@ -7,7 +7,6 @@ import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../. import './ConsolidatedChessTable.css'; import { CHESS_HALF_FILTER_HINT } from '../../../utils/chessMatrixMode'; import { chessTableCellFormatValue } from '../../../utils/chessTableCellFormatValue'; -import { sumAmounts } from '../../../utils/money'; const { Text } = Typography; @@ -21,11 +20,7 @@ interface ConsolidationChessTableI { isHalfFiltered?: boolean; } -type TotalRow = { - id: string; - name: string; - [key: string]: string; // Здесь будут храниться суммы по ID компаний -}; +const DASH = —; export const ConsolidationChessTable: React.FC = ({ data, @@ -91,33 +86,20 @@ export const ConsolidationChessTable: React.FC = ({ }, })); - return [firstColumn, ...companyColumns]; - }, [data?.companies, matrixData, decimals, units]); - - const totalRowData = useMemo(() => { - if (!data?.companies) return null; - - const totalRow: TotalRow = { - id: 'total-row', - name: 'Итого', + // «Итого Дт» — сумма дебетовых значений строки компании; считает сервер (DOC-221277, AURORA-1756) + const debitTotalColumn: ColumnsType[number] = { + title: 'Итого Дт', + key: 'total_debit', + fixed: 'right', + width: 150, + align: 'right', + render: (_: unknown, row: MatrixCompany) => ( + {chessTableCellFormatValue(data.total_debit?.[row.id], units, decimals) || DASH} + ), }; - data.companies.forEach((colCompany) => { - const amountsToSum: string[] = []; - - data.companies.forEach((rowCompany) => { - const cell = matrixData[rowCompany.id]?.[colCompany.id]; - if (cell?.amount_a) { - amountsToSum.push(cell.amount_a); - } - }); - - // сумму, которую не удалось сложить, не выдумываем нулём — ячейка покажет «—» - totalRow[colCompany.id] = sumAmounts(amountsToSum) ?? ''; - }); - - return totalRow as MatrixCompany; - }, [data?.companies, matrixData]); + return [firstColumn, ...companyColumns, debitTotalColumn]; + }, [data?.companies, data?.total_debit, matrixData, decimals, units]); const scrollY = isFullscreen ? 'calc(100vh - 200px)' : 436; @@ -141,27 +123,26 @@ export const ConsolidationChessTable: React.FC = ({ ), }} summary={() => { - if (!totalRowData) return null; + if (!data?.companies.length || isHalfFiltered) return null; + // «Итого Кт» — сумма кредитовых значений столбца компании; пересечение с «Итого Дт» пустое return ( - {!isHalfFiltered && ( - - -
- Итого -
-
+ + +
+ Итого Кт +
+
- {data?.companies.map((company, index) => ( - -
- {chessTableCellFormatValue((totalRowData as TotalRow)[company.id], units, decimals) || '—'} -
-
- ))} -
- )} + {data.companies.map((company, index) => ( + + {chessTableCellFormatValue(data.total_credit?.[company.id], units, decimals) || DASH} + + ))} + + +
); }}