Merge pull request 'AURORA-1757: итоги шахматки из API, фильтр компаний на сервере' (#225) from feat/224-chess-totals-ui into main
This commit is contained in:
commit
4ab792ee39
17
openspec/changes/intercompany-matrix-totals-ui/design.md
Normal file
17
openspec/changes/intercompany-matrix-totals-ui/design.md
Normal file
@ -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` | лишние запросы при быстром выборе — можно добавить задержку |
|
||||
26
openspec/changes/intercompany-matrix-totals-ui/proposal.md
Normal file
26
openspec/changes/intercompany-matrix-totals-ui/proposal.md
Normal file
@ -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).
|
||||
@ -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`, кнопка «Экспорт» матрицы не переходит в состояние загрузки
|
||||
8
openspec/changes/intercompany-matrix-totals-ui/tasks.md
Normal file
8
openspec/changes/intercompany-matrix-totals-ui/tasks.md
Normal file
@ -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
|
||||
@ -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<IntercompanyMatrixResponse>(`${base(versionId)}/all`);
|
||||
export const fetchIntercompanyMatrix = (versionId: string, data: IntercompanyMatrixRequest) =>
|
||||
api.post<IntercompanyMatrixResponse>(base(versionId), data);
|
||||
|
||||
export const fetchChessConsolidationsManual = (versionId: string, data: ChessManualRequest) =>
|
||||
api.post<IntercompanyMatrixResponse>(`${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<IntercompanyMatrixResultResponse>(`${base(versionId)}/result-table`);
|
||||
|
||||
@ -455,11 +455,17 @@ export interface MatrixCell {
|
||||
export interface IntercompanyMatrixResponse {
|
||||
companies: MatrixCompany[];
|
||||
cells: MatrixCell[];
|
||||
/** «Итого Дт»: сумма `amount_a` строки компании, ключ — ID ЮЛ. */
|
||||
total_debit: Record<string, string>;
|
||||
/** «Итого Кт»: сумма `amount_b` столбца компании, ключ — ID ЮЛ. */
|
||||
total_credit: Record<string, string>;
|
||||
}
|
||||
|
||||
export type ChessManualRequest = {
|
||||
/** Тело `POST …/intercompany-matrix`: пустой список — все балансовые счета стороны / весь периметр. */
|
||||
export type IntercompanyMatrixRequest = {
|
||||
debit_account_ids: string[];
|
||||
credit_account_ids: string[];
|
||||
legal_entity_ids: string[];
|
||||
};
|
||||
|
||||
/** Результирующая таблица ВГО (`IntercompanyResultTableOut`): статьи §6 по компаниям. */
|
||||
|
||||
30
web/src/utils/chessMatrixRequest.test.ts
Normal file
30
web/src/utils/chessMatrixRequest.test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
19
web/src/utils/chessMatrixRequest.ts
Normal file
19
web/src/utils/chessMatrixRequest.ts
Normal file
@ -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;
|
||||
@ -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<string[]>([]);
|
||||
// варианты фильтра «Компании» — весь периметр версии; при выборе компаний ответ сужен и их не меняет
|
||||
const [perimeter, setPerimeter] = useState<MatrixCompany[]>([]);
|
||||
const [decimals, setDecimals] = useState(2);
|
||||
const [units, setUnits] = useState('thousand');
|
||||
const [debit, setDebit] = useState<string[]>([]);
|
||||
@ -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',
|
||||
}}>
|
||||
<ConsolidationChessTable
|
||||
data={filteredData}
|
||||
data={data}
|
||||
loading={loading}
|
||||
decimals={decimals}
|
||||
units={units}
|
||||
@ -235,7 +225,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
|
||||
onDebitChange={setDebit}
|
||||
credit={credit}
|
||||
onCreditChange={setCredit}
|
||||
allCompanies={data?.companies || []}
|
||||
allCompanies={perimeter}
|
||||
year={card?.year}
|
||||
quarter={card?.quarter}
|
||||
/>
|
||||
|
||||
@ -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<ConsolidationChessResTableI> =
|
||||
}));
|
||||
|
||||
const totalColumn: ColumnsType<MatrixResCell>[number] = {
|
||||
title: 'Итого',
|
||||
title: 'Итого по группе',
|
||||
dataIndex: 'group_total',
|
||||
key: 'group_total',
|
||||
width: 150,
|
||||
@ -91,44 +90,6 @@ export const ConsolidationChessResTable: React.FC<ConsolidationChessResTableI> =
|
||||
size='medium'
|
||||
bordered
|
||||
title={() => toolbar}
|
||||
summary={() => {
|
||||
if (!data?.articles) return null;
|
||||
|
||||
return (
|
||||
<Table.Summary fixed>
|
||||
<Table.Summary.Row className='resRow'>
|
||||
<Table.Summary.Cell className='resRow' index={0} align='left'>
|
||||
<div>
|
||||
<Text strong>Итого</Text>
|
||||
</div>
|
||||
</Table.Summary.Cell>
|
||||
|
||||
{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 (
|
||||
<Table.Summary.Cell key={company.id} index={index + 1} align='right'>
|
||||
<Text strong>{chessTableCellFormatValue(colTotal, units, decimals) || DASH}</Text>
|
||||
</Table.Summary.Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<Table.Summary.Cell index={data.companies.length + 1} align='right'>
|
||||
<Text strong>
|
||||
{chessTableCellFormatValue(sumAmounts(data.articles.map((a) => a.group_total)) ?? '', units, decimals) || DASH}
|
||||
</Text>
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
</Table.Summary>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 = <span style={{ color: '#bfbfbf' }}>—</span>;
|
||||
|
||||
export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
||||
data,
|
||||
@ -91,33 +86,20 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
||||
},
|
||||
}));
|
||||
|
||||
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<MatrixCompany>[number] = {
|
||||
title: 'Итого Дт',
|
||||
key: 'total_debit',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
render: (_: unknown, row: MatrixCompany) => (
|
||||
<Text strong>{chessTableCellFormatValue(data.total_debit?.[row.id], units, decimals) || DASH}</Text>
|
||||
),
|
||||
};
|
||||
|
||||
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<ConsolidationChessTableI> = ({
|
||||
),
|
||||
}}
|
||||
summary={() => {
|
||||
if (!totalRowData) return null;
|
||||
if (!data?.companies.length || isHalfFiltered) return null;
|
||||
|
||||
// «Итого Кт» — сумма кредитовых значений столбца компании; пересечение с «Итого Дт» пустое
|
||||
return (
|
||||
<Table.Summary fixed>
|
||||
{!isHalfFiltered && (
|
||||
<Table.Summary.Row className='resRow'>
|
||||
<Table.Summary.Cell className='resRow' index={0} align='left'>
|
||||
<div style={{ backgroundColor: '#e6f4ea', width: '100%', height: '100%' }}>
|
||||
<Text strong>Итого</Text>
|
||||
</div>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Row className='resRow'>
|
||||
<Table.Summary.Cell className='resRow' index={0} align='left'>
|
||||
<div style={{ backgroundColor: '#e6f4ea', width: '100%', height: '100%' }}>
|
||||
<Text strong>Итого Кт</Text>
|
||||
</div>
|
||||
</Table.Summary.Cell>
|
||||
|
||||
{data?.companies.map((company, index) => (
|
||||
<Table.Summary.Cell className='resRow' key={company.id} index={index + 1} align='right'>
|
||||
<div>
|
||||
<Text strong>{chessTableCellFormatValue((totalRowData as TotalRow)[company.id], units, decimals) || '—'}</Text>
|
||||
</div>
|
||||
</Table.Summary.Cell>
|
||||
))}
|
||||
</Table.Summary.Row>
|
||||
)}
|
||||
{data.companies.map((company, index) => (
|
||||
<Table.Summary.Cell className='resRow' key={company.id} index={index + 1} align='right'>
|
||||
<Text strong>{chessTableCellFormatValue(data.total_credit?.[company.id], units, decimals) || DASH}</Text>
|
||||
</Table.Summary.Cell>
|
||||
))}
|
||||
|
||||
<Table.Summary.Cell className='resRow' index={data.companies.length + 1} />
|
||||
</Table.Summary.Row>
|
||||
</Table.Summary>
|
||||
);
|
||||
}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user