Шахматка: при выборе одной стороны Дт/Кт не шлём запрос (ревью #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[], pairs: readonly CounterPairOut[],
storage: Pick<Storage, 'getItem'> | null, storage: Pick<Storage, 'getItem'> | null,
): boolean => { ): boolean => {
if (pairs?.length === 0) return false; if (pairs.length === 0) return false;
if (status && FINISHED_STATUSES.includes(status)) return false; if (status && FINISHED_STATUSES.includes(status)) return false;
try { try {
return storage?.getItem(`${COUNTER_PAIRS_STORAGE_PREFIX}${versionId}`) !== counterPairsKey(pairs); 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 { useAppSelector } from '../../../store/store';
import type { RelatedGroupOut } from '../../../types/api'; import type { RelatedGroupOut } from '../../../types/api';
import './CatalogueCompanyGroupTabs.css'; import './CatalogueCompanyGroupTabs.css';
interface CataloguGroupTabsI { interface CatalogueCompanyGroupTabsI {
activeTab: CatalogueCompanyGroupTabKey; activeTab: CatalogueCompanyGroupTabKey;
setActiveTab: (val: CatalogueCompanyGroupTabKey) => void; setActiveTab: (val: CatalogueCompanyGroupTabKey) => void;
gskData: RelatedGroupOut[]; gskData: RelatedGroupOut[];
@ -11,7 +11,7 @@ interface CataloguGroupTabsI {
export type CatalogueCompanyGroupTabKey = 'legal_entities' | 'gsk' | 'gsz'; 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); const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup);
return ( return (

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import type React from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../../types/api'; import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../../types/api';
import './ConsolidatedChessTable.css'; import './ConsolidatedChessTable.css';
import { CHESS_HALF_FILTER_HINT } from '../../../utils/chessMatrixMode';
import { chessTableCellFormatValue } from '../../../utils/chessTableCellFormatValue'; import { chessTableCellFormatValue } from '../../../utils/chessTableCellFormatValue';
import { sumAmounts } from '../../../utils/money'; import { sumAmounts } from '../../../utils/money';
@ -111,6 +112,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
} }
}); });
// сумму, которую не удалось сложить, не выдумываем нулём — ячейка покажет «—»
totalRow[colCompany.id] = sumAmounts(amountsToSum) ?? ''; totalRow[colCompany.id] = sumAmounts(amountsToSum) ?? '';
}); });
@ -121,9 +123,9 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
return ( return (
<Table<MatrixCompany> <Table<MatrixCompany>
className={!isFullscreen ? 'chessTable customHeight' : 'chessTable'} className={isHalfFiltered && !isFullscreen ? 'chessTable customHeight' : 'chessTable'}
dataSource={isHalfFiltered ? [] : data?.companies || []} dataSource={isHalfFiltered ? [] : data?.companies || []}
columns={isHalfFiltered ? [] : columns || []} columns={isHalfFiltered ? [] : columns}
rowKey='id' rowKey='id'
pagination={false} pagination={false}
loading={loading} loading={loading}
@ -133,7 +135,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
title={() => toolbar} title={() => toolbar}
locale={{ locale={{
emptyText: isHalfFiltered ? ( 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 { ApiError, api } from '../../../api/client';
import { type OptionType, TreeFilterDropdown } from '../../../components/TreeFilterDropdown/TreeFilterDropdown'; import { type OptionType, TreeFilterDropdown } from '../../../components/TreeFilterDropdown/TreeFilterDropdown';
import type { Account, MatrixCompany, Quarter } from '../../../types/api'; import type { Account, MatrixCompany, Quarter } from '../../../types/api';
import { CHESS_HALF_FILTER_HINT, chessMatrixMode } from '../../../utils/chessMatrixMode';
import { quarterShortLabel } from '../../../utils/labels'; import { quarterShortLabel } from '../../../utils/labels';
import { useNotificationApi } from '../../../utils/useNotification'; import { useNotificationApi } from '../../../utils/useNotification';
@ -109,7 +110,8 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
loadAccounts(); loadAccounts();
}, [notification]); }, [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 ( return (
<Space size='middle' wrap> <Space size='middle' wrap>