Andrey Epifancev d3c4d35dca Шахматка: при выборе одной стороны Дт/Кт не шлём запрос (ревью #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>
2026-10-29 21:38:40 +04:00

323 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import { Button, ConfigProvider } from 'antd';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ApiError } from '../../../api/client';
import {
exportChessConsolidationsResult,
exportChessTableAll,
exportChessTableManual,
fetchChessConsolidations,
fetchChessConsolidationsManual,
fetchChessConsolidationsResult,
} 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';
import { ConsolidationChessResTable } from '../ConsolidationChessResTable/ConsolidationChessResTable';
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable';
import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter';
export const ChessContent = ({ card }: { card: VersionCard | null }) => {
const { version_id } = useParams<{ version_id: string }>();
const notification = useNotificationApi();
const [data, setData] = useState<IntercompanyMatrixResponse | null>(null);
const [loading, setLoading] = useState(false);
const [matrixExportLoading, setMatrixExportLoading] = useState(false);
const [resExportLoading, setResExportLoading] = useState(false);
const [companies, setCompanies] = useState<string[]>([]);
const [decimals, setDecimals] = useState(2);
const [units, setUnits] = useState('thousand');
const [debit, setDebit] = useState<string[]>([]);
const [credit, setCredit] = useState<string[]>([]);
const [chessResData, setChessResData] = useState<IntercompanyMatrixResultResponse | null>(null);
const [chessResUnits, setChessResUnits] = useState('thousand');
const [chessResDecimals, setChessResDecimals] = useState(2);
const [chessResLoading, setChessResLoading] = useState(false);
const chessTableRef = useRef<HTMLDivElement>(null);
const resTableRef = useRef<HTMLDivElement>(null);
const [isChessFullscreen, setIsChessFullscreen] = useState(false);
const [isResFullscreen, setIsResFullscreen] = useState(false);
const toggleFullscreen = async (ref: React.RefObject<HTMLDivElement>, setFullscreen: (val: boolean) => void) => {
if (!ref.current) return;
try {
if (!document.fullscreenElement) {
await ref.current.requestFullscreen();
setFullscreen(true);
} else {
await document.exitFullscreen();
setFullscreen(false);
}
} catch (e) {
notification.error({
description: e instanceof Error ? e.message : 'Не удалось переключить полноэкранный режим',
});
}
};
useEffect(() => {
const handleFullscreenChange = () => {
if (!document.fullscreenElement) {
setIsChessFullscreen(false);
setIsResFullscreen(false);
}
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);
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 (chessMatrixMode(dIds, cIds) === 'manual') {
res = await fetchChessConsolidationsManual(vId, { debit_account_ids: dIds, credit_account_ids: cIds });
} else {
res = await fetchChessConsolidations(vId);
}
if (isActual()) {
setData(res);
}
} catch (e) {
if (isActual() && e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
if (isActual()) {
setLoading(false);
}
}
},
[notification],
);
const loadResData = useCallback(
async (vid: string) => {
setChessResLoading(true);
try {
const res = await fetchChessConsolidationsResult(vid);
setChessResData(res);
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
setChessResLoading(false);
}
},
[notification],
);
const versionInfo = card ? { groupName: card.company_group.name, year: card.year, quarter: card.quarter } : null;
const download = async () => {
if (!version_id) {
return;
}
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));
}
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
setMatrixExportLoading(false);
}
};
const downloadResTable = async () => {
if (!version_id) {
return;
}
setResExportLoading(true);
try {
const { blob, filename } = await exportChessConsolidationsResult(version_id);
saveBlob(blob, filename ?? xlsxFilename('Результирующая_таблица_ВГО', versionInfo));
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
setResExportLoading(false);
}
};
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;
let isMounted = true;
const isActual = () => isMounted;
loadData(version_id, debit, credit, isActual);
return () => {
isMounted = false;
setLoading(false);
};
}, [version_id, debit, credit, isHalfFiltered, loadData]);
useEffect(() => {
if (!version_id) return;
loadResData(version_id);
}, [version_id, loadResData]);
return (
<ConfigProvider getPopupContainer={() => (document.fullscreenElement as HTMLElement) || document.body}>
<div
style={{
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
overflowX: 'hidden',
gap: 24,
paddingBottom: 32,
width: '100%',
}}>
<div
ref={chessTableRef}
style={{
backgroundColor: isChessFullscreen ? '#fff' : 'transparent',
padding: isChessFullscreen ? 6 : 0,
height: isChessFullscreen ? '100vh' : 'auto',
overflow: isChessFullscreen ? 'hidden' : 'visible',
display: 'flex',
flexDirection: 'column',
}}>
<ConsolidationChessTable
data={filteredData}
loading={loading}
decimals={decimals}
units={units}
isFullscreen={isChessFullscreen}
isHalfFiltered={isHalfFiltered}
toolbar={
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
flexWrap: 'wrap',
gap: 16,
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}>
<ConsolidationChessTableFilter
companies={companies}
onCompaniesChange={setCompanies}
debit={debit}
onDebitChange={setDebit}
credit={credit}
onCreditChange={setCredit}
allCompanies={data?.companies || []}
year={card?.year}
quarter={card?.quarter}
/>
<div
style={{
width: 1,
height: 50,
backgroundColor: '#e5e7eb',
}}
/>
<ConsolidationChessFormatTableFilter
decimals={decimals}
units={units}
onDecimalsChange={setDecimals}
onUnitsChange={setUnits}
/>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<Button disabled={isHalfFiltered} loading={matrixExportLoading} onClick={() => download()} icon={<DownloadOutlined />}>
Экспорт
</Button>
<Button
onClick={() => toggleFullscreen(chessTableRef, setIsChessFullscreen)}
icon={isChessFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
{isChessFullscreen ? 'Свернуть' : 'На весь экран'}
</Button>
</div>
</div>
}
/>
</div>
<div
ref={resTableRef}
style={{
backgroundColor: isResFullscreen ? '#fff' : 'transparent',
padding: isResFullscreen ? 6 : 0,
height: isResFullscreen ? '100vh' : 'auto',
overflow: isResFullscreen ? 'hidden' : 'visible',
display: 'flex',
flexDirection: 'column',
}}>
<ConsolidationChessResTable
data={chessResData}
loading={chessResLoading}
decimals={chessResDecimals}
units={chessResUnits}
isFullscreen={isResFullscreen}
toolbar={
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
flexWrap: 'wrap',
gap: 16,
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}>
<ConsolidationChessFormatTableFilter
decimals={chessResDecimals}
units={chessResUnits}
onDecimalsChange={setChessResDecimals}
onUnitsChange={setChessResUnits}
/>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<Button loading={resExportLoading} onClick={() => downloadResTable()} icon={<DownloadOutlined />}>
Экспорт
</Button>
<Button
onClick={() => toggleFullscreen(resTableRef, setIsResFullscreen)}
icon={isResFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
{isResFullscreen ? 'Свернуть' : 'На весь экран'}
</Button>
</div>
</div>
}
/>
</div>
</div>
</ConfigProvider>
);
};