Шахматка: при выборе одной стороны Дт/Кт не шлём запрос (ревью #207)

- режим запроса вынесен в utils/chessMatrixMode (all / manual / half) с тестами;
  при «одна сторона» запрос матрицы не уходит, индикатор загрузки снимается;
- подсказка по спеке intercompany-matrix-ui: один текст в таблице и у фильтров,
  у фильтров — только в состоянии «одна сторона»;
- высота шахматки: min-height только для состояния подсказки, обёртка снова auto
  (в обычном режиме «Итого» сразу под строками);
- откат pairs?.length в shouldWarnCounterPairs (маскировал старый API);
- вернули комментарий про сумму, лишний columns || [] и дубль CSS убраны;
  опечатка CataloguGroupTabsI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-29 21:38:40 +04:00
parent 65c44978bd
commit d3c4d35dca
8 changed files with 55 additions and 24 deletions

View File

@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { chessMatrixMode } from './chessMatrixMode';
describe('chessMatrixMode', () => {
it('фильтры пусты — полная матрица', () => {
expect(chessMatrixMode([], [])).toBe('all');
});
it('выбраны Дт и Кт — ручной расчёт', () => {
expect(chessMatrixMode(['62'], ['60'])).toBe('manual');
});
it('только Дт — одна сторона', () => {
expect(chessMatrixMode(['62'], [])).toBe('half');
});
it('только Кт — одна сторона', () => {
expect(chessMatrixMode([], ['60'])).toBe('half');
});
});

View File

@ -0,0 +1,16 @@
/**
* Режим запроса шахматки по фильтрам счетов:
* - `all` — фильтры пусты, берём полную матрицу;
* - `manual` — выбраны и Дт, и Кт, считаем по выбранным счетам;
* - `half` — выбрана только одна сторона: запрос не шлём, показываем подсказку.
*/
export type ChessMatrixMode = 'all' | 'manual' | 'half';
/** Текст для состояния «выбрана одна сторона» — в таблице и у фильтров один и тот же (openspec intercompany-matrix-ui). */
export const CHESS_HALF_FILTER_HINT = 'Для расчёта по счетам выберите счета и в Дт, и в Кт';
export const chessMatrixMode = (debit: readonly string[], credit: readonly string[]): ChessMatrixMode => {
if (debit.length > 0 && credit.length > 0) return 'manual';
if (debit.length === 0 && credit.length === 0) return 'all';
return 'half';
};

View File

@ -140,7 +140,7 @@ export const shouldWarnCounterPairs = (
pairs: readonly CounterPairOut[],
storage: Pick<Storage, 'getItem'> | null,
): boolean => {
if (pairs?.length === 0) return false;
if (pairs.length === 0) return false;
if (status && FINISHED_STATUSES.includes(status)) return false;
try {
return storage?.getItem(`${COUNTER_PAIRS_STORAGE_PREFIX}${versionId}`) !== counterPairsKey(pairs);

View File

@ -2,7 +2,7 @@ import { Tabs } from 'antd';
import { useAppSelector } from '../../../store/store';
import type { RelatedGroupOut } from '../../../types/api';
import './CatalogueCompanyGroupTabs.css';
interface CataloguGroupTabsI {
interface CatalogueCompanyGroupTabsI {
activeTab: CatalogueCompanyGroupTabKey;
setActiveTab: (val: CatalogueCompanyGroupTabKey) => void;
gskData: RelatedGroupOut[];
@ -11,7 +11,7 @@ interface CataloguGroupTabsI {
export type CatalogueCompanyGroupTabKey = 'legal_entities' | 'gsk' | 'gsz';
export const CatalogueCompanyGroupTabs = ({ activeTab, setActiveTab, gskData, gszData }: CataloguGroupTabsI) => {
export const CatalogueCompanyGroupTabs = ({ activeTab, setActiveTab, gskData, gszData }: CatalogueCompanyGroupTabsI) => {
const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup);
return (

View File

@ -13,6 +13,7 @@ import {
} from '../../../services/api/intercompanyMatrix';
import type { IntercompanyMatrixResponse, IntercompanyMatrixResultResponse } from '../../../types/api';
import type { VersionCard } from '../../../types/consolidations/version_members';
import { chessMatrixMode } from '../../../utils/chessMatrixMode';
import { saveBlob, xlsxFilename } from '../../../utils/download';
import { useNotificationApi } from '../../../utils/useNotification';
import { ConsolidationChessFormatTableFilter } from '../ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter';
@ -72,14 +73,15 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);
const isHalfFiltered = (debit.length > 0 && credit.length === 0) || (debit.length === 0 && credit.length > 0);
const matrixMode = chessMatrixMode(debit, credit);
const isHalfFiltered = matrixMode === 'half';
const loadData = useCallback(
async (vId: string, dIds: string[], cIds: string[], isActual: () => boolean) => {
setLoading(true);
try {
let res: IntercompanyMatrixResponse;
if (dIds?.length && cIds?.length) {
if (chessMatrixMode(dIds, cIds) === 'manual') {
res = await fetchChessConsolidationsManual(vId, { debit_account_ids: dIds, credit_account_ids: cIds });
} else {
res = await fetchChessConsolidations(vId);
@ -97,7 +99,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
}
}
},
[notification, isHalfFiltered],
[notification],
);
const loadResData = useCallback(
async (vid: string) => {
@ -124,7 +126,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
}
setMatrixExportLoading(true);
try {
if (debit?.length && credit?.length) {
if (matrixMode === 'manual') {
const { blob, filename } = await exportChessTableManual(version_id, { debit_account_ids: debit, credit_account_ids: credit });
saveBlob(blob, filename ?? xlsxFilename('Шахматка', versionInfo));
} else {
@ -167,7 +169,8 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
}, [data, companies]);
useEffect(() => {
if (!version_id) return;
// выбрана только одна сторона Дт/Кт — запрос не шлём, таблица показывает подсказку
if (!version_id || isHalfFiltered) return;
let isMounted = true;
const isActual = () => isMounted;
@ -178,7 +181,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
isMounted = false;
setLoading(false);
};
}, [version_id, debit, credit, loadData]);
}, [version_id, debit, credit, isHalfFiltered, loadData]);
useEffect(() => {
if (!version_id) return;
@ -202,7 +205,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
style={{
backgroundColor: isChessFullscreen ? '#fff' : 'transparent',
padding: isChessFullscreen ? 6 : 0,
height: isChessFullscreen ? '100vh' : '100%',
height: isChessFullscreen ? '100vh' : 'auto',
overflow: isChessFullscreen ? 'hidden' : 'visible',
display: 'flex',
flexDirection: 'column',

View File

@ -17,15 +17,6 @@
background-color: #e6f4ea !important ;
}
.chessTable .ant-table-container {
display: flex;
flex-direction: column;
}
.chessTable.customHeight .ant-table-container {
height: 550px !important;
}
.chessTable .ant-table-summary {
margin-top: auto;
min-height: 550px;
}

View File

@ -5,6 +5,7 @@ import type React from 'react';
import { useMemo } from 'react';
import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../../types/api';
import './ConsolidatedChessTable.css';
import { CHESS_HALF_FILTER_HINT } from '../../../utils/chessMatrixMode';
import { chessTableCellFormatValue } from '../../../utils/chessTableCellFormatValue';
import { sumAmounts } from '../../../utils/money';
@ -111,6 +112,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
}
});
// сумму, которую не удалось сложить, не выдумываем нулём — ячейка покажет «—»
totalRow[colCompany.id] = sumAmounts(amountsToSum) ?? '';
});
@ -121,9 +123,9 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
return (
<Table<MatrixCompany>
className={!isFullscreen ? 'chessTable customHeight' : 'chessTable'}
className={isHalfFiltered && !isFullscreen ? 'chessTable customHeight' : 'chessTable'}
dataSource={isHalfFiltered ? [] : data?.companies || []}
columns={isHalfFiltered ? [] : columns || []}
columns={isHalfFiltered ? [] : columns}
rowKey='id'
pagination={false}
loading={loading}
@ -133,7 +135,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
title={() => toolbar}
locale={{
emptyText: isHalfFiltered ? (
<div style={{ padding: '40px 0', color: '#8c8c8c', fontSize: 16 }}>Для получения данных выберите счета по Дт и Кт</div>
<div style={{ padding: '40px 0', color: '#8c8c8c', fontSize: 16 }}>{CHESS_HALF_FILTER_HINT}</div>
) : (
'Нет данных'
),

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { ApiError, api } from '../../../api/client';
import { type OptionType, TreeFilterDropdown } from '../../../components/TreeFilterDropdown/TreeFilterDropdown';
import type { Account, MatrixCompany, Quarter } from '../../../types/api';
import { CHESS_HALF_FILTER_HINT, chessMatrixMode } from '../../../utils/chessMatrixMode';
import { quarterShortLabel } from '../../../utils/labels';
import { useNotificationApi } from '../../../utils/useNotification';
@ -109,7 +110,8 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
loadAccounts();
}, [notification]);
const warningBlock = <span style={{ color: '#d46b08', fontSize: 13 }}>Выберите счета по Дт и Кт для фильтрации</span>;
const warningBlock =
chessMatrixMode(debit, credit) === 'half' ? <span style={{ color: '#d46b08', fontSize: 13 }}>{CHESS_HALF_FILTER_HINT}</span> : null;
return (
<Space size='middle' wrap>