добавленна результирующая таблицв ВГО

This commit is contained in:
Grog 2026-10-22 10:07:33 +03:00
parent cbeb9daa38
commit d1c0f93360
9 changed files with 554 additions and 286 deletions

View File

@ -60,21 +60,23 @@ function ReportContent() {
}, [tab, navigation, pathname]); }, [tab, navigation, pathname]);
return ( return (
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', height: '100vh', gap: 12 }}> <div style={{ padding: 12, display: 'flex', flexDirection: 'column', height: '100vh', gap: 12 }}>
<PageTitle <div style={{ flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 12 }}>
backable <PageTitle
location={['Отчётность', groupName, period]} backable
header={card ? `${groupName} — ${card.year} ${quarterShortLabel(card.quarter)}` : 'Сводная ОСВ'} location={['Отчётность', groupName, period]}
description={russianStatus} header={card ? `${groupName} — ${card.year} ${quarterShortLabel(card.quarter)}` : 'Сводная ОСВ'}
extra={ description={russianStatus}
card && ( extra={
<Space size={8}> card && (
<span style={{ color: '#667085', fontSize: 12 }}>Статус отчёта</span> <Space size={8}>
<TableTag text={russianStatus} {...preset} /> <span style={{ color: '#667085', fontSize: 12 }}>Статус отчёта</span>
</Space> <TableTag text={russianStatus} {...preset} />
) </Space>
} )
/> }
<OsvTabs /> />
<OsvTabs />
</div>
{ActiveTab} {ActiveTab}
</div> </div>
); );

View File

@ -1,5 +1,5 @@
import { api, apiFetchBlob } from '../../api/client'; import { api, apiFetchBlob } from '../../api/client';
import type { ChessManualRequest, IntercompanyMatrixResponse } from '../../types/chess/chess'; import type { ChessManualRequest, IntercompanyMatrixResponse, IntercompanyMatrixResultResponse } from '../../types/chess/chess';
export const fetchChessConsolidations = (id: string) => export const fetchChessConsolidations = (id: string) =>
api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/all`); api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/all`);
@ -20,4 +20,4 @@ export const exportChessTableManual = (versionId: string, data: ChessManualReque
}); });
export const fetchChessConsolidationsResult = (id: string) => export const fetchChessConsolidationsResult = (id: string) =>
api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/result-table`); api.get<IntercompanyMatrixResultResponse>(`/consolidations/versions/${id}/intercompany-matrix/result-table`);

View File

@ -0,0 +1,44 @@
export const chessTableCellFormatValue = (val: string | number | null | undefined, units: string, decimals: number) => {
if (val === undefined || val === null || val === '') return '';
let strVal = String(val);
if (strVal.startsWith('(') && strVal.endsWith(')')) {
strVal = `-${strVal.slice(1, -1)}`;
}
if (units !== 'rub') {
let shift = 0;
if (units === 'thousand') shift = 3;
else if (units === 'million') shift = 6;
else if (units === 'billion') shift = 9;
if (shift > 0) {
const isNegative = strVal.startsWith('-');
const clean = isNegative ? strVal.slice(1) : strVal;
const [intPart = '0', fracPart = ''] = clean.split('.');
let newInt = intPart;
let newFrac = fracPart;
if (intPart.length <= shift) {
const zeros = '0'.repeat(shift - intPart.length);
newInt = '0';
newFrac = `${zeros}${intPart}${fracPart}`;
} else {
newInt = intPart.slice(0, -shift);
newFrac = `${intPart.slice(-shift)}${fracPart}`;
}
strVal = `${isNegative ? '-' : ''}${newInt}.${newFrac.slice(0, 4)}`;
}
}
const num = Number(strVal);
if (Number.isNaN(num)) return strVal;
return new Intl.NumberFormat('ru-RU', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(num);
};

View File

@ -8,14 +8,16 @@ import {
exportChessTableManual, exportChessTableManual,
fetchChessConsolidations, fetchChessConsolidations,
fetchChessConsolidationsManual, fetchChessConsolidationsManual,
fetchChessConsolidationsResult,
} from '../../services/consodilation/intercompanyMatrix'; } from '../../services/consodilation/intercompanyMatrix';
import type { IntercompanyMatrixResponse } from '../../types/chess/chess'; import type { IntercompanyMatrixResponse, IntercompanyMatrixResultResponse } from '../../types/chess/chess';
import type { VersionCard } from '../../types/consolidations/version_members';
import { exportFilename, saveBlob } from '../../utils/download'; import { exportFilename, saveBlob } from '../../utils/download';
import { useNotificationApi } from '../../utils/useNotification'; import { useNotificationApi } from '../../utils/useNotification';
import { ConsolidationChessFormatTableFilter } from '../ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter';
import { ConsolidationChessResTable } from '../ConsolidationChessResTable/ConsolidationChessResTable';
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable'; import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable';
import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter'; import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter';
import { VersionCard } from '../../types/consolidations/version_members';
export const ChessContent = ({ card }: { card: VersionCard | null }) => { export const ChessContent = ({ card }: { card: VersionCard | null }) => {
const { version_id } = useParams<{ version_id: string }>(); const { version_id } = useParams<{ version_id: string }>();
@ -29,26 +31,48 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
const [units, setUnits] = useState('thousand'); const [units, setUnits] = useState('thousand');
const [debit, setDebit] = useState<string[]>([]); const [debit, setDebit] = useState<string[]>([]);
const [credit, setCredit] = useState<string[]>([]); const [credit, setCredit] = useState<string[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const toggleFullscreen = async () => { const [chessResData, setChessResData] = useState<IntercompanyMatrixResultResponse | null>(null);
if (!containerRef.current) return; 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 { try {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
await containerRef.current.requestFullscreen(); await ref.current.requestFullscreen();
setFullscreen(true);
} else { } else {
await document.exitFullscreen(); await document.exitFullscreen();
setFullscreen(false);
} }
} catch (e) { } catch (e) {
console.log(e); if (e instanceof ApiError) {
notification.error({ description: e.message });
}
} }
}; };
useEffect(() => {
const handleFullscreenChange = () => {
if (!document.fullscreenElement) {
setIsChessFullscreen(false);
setIsResFullscreen(false);
}
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);
const loadData = async (vId: string, dIds: string[], cIds: string[], isActual: () => boolean) => { const loadData = async (vId: string, dIds: string[], cIds: string[], isActual: () => boolean) => {
if ((dIds.length > 0 && cIds.length === 0) || (dIds.length === 0 && cIds.length > 0)) { if ((dIds.length > 0 && cIds.length === 0) || (dIds.length === 0 && cIds.length > 0)) {
notification.info({description:'Для сравнений по Дт и Кт нужно заполнить оба фильтра'}) notification.info({ description: 'Для сравнений по Дт и Кт нужно заполнить оба фильтра' });
return; return;
} }
setLoading(true); setLoading(true);
@ -72,6 +96,19 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
} }
} }
}; };
const loadResData = 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);
}
};
const download = async () => { const download = async () => {
if (!version_id) { if (!version_id) {
@ -84,7 +121,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
saveBlob(blob, filename ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? '')); saveBlob(blob, filename ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? ''));
} else { } else {
const { blob, filename } = await exportChessTableAll(version_id); const { blob, filename } = await exportChessTableAll(version_id);
saveBlob(blob, filename ?? 'Шахматка'); saveBlob(blob, filename ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? ''));
} }
} catch (e) { } catch (e) {
if (e instanceof ApiError) { if (e instanceof ApiError) {
@ -95,6 +132,16 @@ 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(() => { useEffect(() => {
if (!version_id) return; if (!version_id) return;
@ -109,77 +156,132 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
}, [version_id, debit, credit]); }, [version_id, debit, credit]);
useEffect(() => { useEffect(() => {
const handleFullscreenChange = () => { if (!version_id) return;
setIsFullscreen(!!document.fullscreenElement); loadResData(version_id);
}; }, [version_id]);
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
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]);
return ( return (
<ConfigProvider getPopupContainer={() => containerRef.current || document.body}> <ConfigProvider getPopupContainer={() => (document.fullscreenElement as HTMLElement) || document.body}>
<div <div
ref={containerRef}
style={{ style={{
backgroundColor: isFullscreen ? '#fff' : 'transparent',
padding: isFullscreen ? 24 : 0,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: isFullscreen ? '100vh' : 'auto', overflowY: 'auto',
overflow: isFullscreen ? 'hidden' : 'visible', overflowX: 'hidden',
gap: 24,
paddingBottom: 32,
width: '100%',
}}> }}>
<ConsolidationChessTable <div
data={filteredData} ref={chessTableRef}
loading={loading} style={{
decimals={decimals} backgroundColor: isChessFullscreen ? '#fff' : 'transparent',
units={units} padding: isChessFullscreen ? 24 : 0,
isFullscreen={isFullscreen} height: isChessFullscreen ? '100vh' : 'auto',
toolbar={ overflow: isChessFullscreen ? 'hidden' : 'visible',
<div display: 'flex',
style={{ flexDirection: 'column',
display: 'flex', }}>
justifyContent: 'space-between', <ConsolidationChessTable
alignItems: 'center', data={filteredData}
width: '100%', loading={loading}
flexWrap: 'wrap', decimals={decimals}
gap: 16, units={units}
}}> isFullscreen={isChessFullscreen}
<div style={{ flex: '1 1 auto' }}> toolbar={
<ConsolidationChessTableFilter <div
companies={companies} style={{
onCompaniesChange={setCompanies} display: 'flex',
decimals={decimals} justifyContent: 'space-between',
onDecimalsChange={setDecimals} alignItems: 'center',
units={units} width: '100%',
onUnitsChange={setUnits} flexWrap: 'wrap',
debit={debit} gap: 16,
onDebitChange={setDebit} }}>
credit={credit} <div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}>
onCreditChange={setCredit} <ConsolidationChessTableFilter
allCompanies={data?.companies || []} companies={companies}
/> onCompaniesChange={setCompanies}
debit={debit}
onDebitChange={setDebit}
credit={credit}
onCreditChange={setCredit}
allCompanies={data?.companies || []}
/>
<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 loading={expoerLoading} onClick={() => download()} icon={<DownloadOutlined />}>
Экспорт
</Button>
<Button
onClick={() => toggleFullscreen(chessTableRef, setIsChessFullscreen)}
icon={isChessFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
{isChessFullscreen ? 'Свернуть' : 'На весь экран'}
</Button>
</div>
</div> </div>
<Button loading={expoerLoading} onClick={() => download()} icon={<DownloadOutlined />}> }
Экспорт />
</Button> </div>
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
{isFullscreen ? 'Свернуть' : 'На весь экран'} <div
</Button> ref={resTableRef}
</div> style={{
} backgroundColor: isResFullscreen ? '#fff' : 'transparent',
/> padding: isResFullscreen ? 24 : 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
onClick={() => toggleFullscreen(resTableRef, setIsResFullscreen)}
icon={isResFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
{isResFullscreen ? 'Свернуть' : 'На весь экран'}
</Button>
</div>
</div>
}
/>
</div>
</div> </div>
</ConfigProvider> </ConfigProvider>
); );

View File

@ -0,0 +1,50 @@
import { Select, Space } from 'antd';
import type React from 'react';
interface ConsolidationChessFormatTableFilterI {
decimals: number;
onDecimalsChange: (val: number) => void;
units: string;
onUnitsChange: (val: string) => void;
}
export const ConsolidationChessFormatTableFilter: React.FC<ConsolidationChessFormatTableFilterI> = ({
decimals,
onDecimalsChange,
units,
onUnitsChange,
}) => {
const UNIT_OPTIONS = [
{ value: 'thousand', label: 'тыс. руб.' },
{ value: 'million', label: 'млн. руб.' },
{ value: 'billion', label: 'млрд руб.' },
];
const DECIMALS_OPTIONS = [
{ value: 0, label: '0' },
{ value: 1, label: '1' },
{ value: 2, label: '2' },
{ value: 3, label: '3' },
];
return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 16 }}>
<Space size='middle' wrap>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Единицы</span>
<Select style={{ minWidth: 120 }} options={UNIT_OPTIONS} value={units} onChange={onUnitsChange} variant='underlined' />
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Знаков после запятой</span>
<Select
style={{ minWidth: 150 }}
value={decimals}
onChange={(v) => onDecimalsChange(v || 0)}
options={DECIMALS_OPTIONS}
variant='underlined'
/>
</Space>
</Space>
</div>
);
};

View File

@ -0,0 +1,11 @@
.ant-table-summary {
border-end-end-radius: 8px !important;
border-end-start-radius: 8px !important;
}
.ant-table-cell-fix {
background-color: #f8fafc !important;
}
.resRow {
background-color: #e6f4ea !important ;
}

View File

@ -0,0 +1,148 @@
import { Table, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type React from 'react';
import { useMemo } from 'react';
import type { IntercompanyMatrixResultResponse, MatrixResCell } from '../../types/chess/chess';
import './ConsolidatedChessResTable.css';
import { chessTableCellFormatValue } from '../../utils/chessTableCellFormatValue';
const { Text } = Typography;
interface ConsolidationChessResTableI {
data: IntercompanyMatrixResultResponse | null;
loading: boolean;
decimals: number;
units: string;
toolbar?: React.ReactNode;
isFullscreen?: boolean;
}
export const ConsolidationChessResTable: React.FC<ConsolidationChessResTableI> = ({
data,
loading,
decimals,
units,
toolbar,
isFullscreen = false,
}) => {
const matrixData = useMemo(() => {
if (!data?.articles) return {};
const result: Record<string, Record<string, string>> = {};
data.articles.forEach((article) => {
result[article.code] = {};
article.amounts.forEach((amount) => {
result[article.code][amount.company_id] = amount.amount;
});
});
return result;
}, [data?.articles]);
const columns: ColumnsType<MatrixResCell> = useMemo(() => {
if (!data?.companies) return [];
const firstColumn: ColumnsType<MatrixResCell>[number] = {
title: 'Статья',
dataIndex: 'name',
key: 'name',
fixed: 'left',
width: 250,
};
const companyColumns = data.companies.map((company) => ({
title: company.name,
dataIndex: company.id,
key: company.id,
width: 150,
align: 'right' as const,
render: (_: unknown, row: MatrixResCell) => {
const amount = matrixData[row.code]?.[company.id];
const formatted = chessTableCellFormatValue(amount, units, decimals);
if (formatted === '—') {
return <span style={{ color: '#bfbfbf' }}>—</span>;
}
return formatted;
},
}));
const totalColumn: ColumnsType<MatrixResCell>[number] = {
title: 'Итого',
dataIndex: 'group_total',
key: 'group_total',
width: 150,
align: 'right',
render: (val: string) => <Text strong>{chessTableCellFormatValue(val, units, decimals)}</Text>,
};
return [firstColumn, ...companyColumns, totalColumn];
}, [data?.companies, matrixData, decimals, units]);
// 4. Считаем итоговую строку (сумма всех group_total)
const summaryTotal = useMemo(() => {
if (!data?.articles) return '0';
let sum = 0;
data.articles.forEach((article) => {
sum += Number(article.group_total || 0);
});
return String(sum);
}, [data?.articles]);
const scrollY = isFullscreen ? 'calc(100vh - 180px)' : 436;
return (
<Table<MatrixResCell>
className='chessTable'
dataSource={data?.articles || []}
columns={columns}
rowKey='code'
pagination={false}
loading={loading}
scroll={{ x: 'max-content', y: scrollY }}
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) => {
let colTotal = 0;
data.articles.forEach((article) => {
const amount = matrixData[article.code]?.[company.id];
if (amount && amount !== '0') {
colTotal += Number(amount);
}
});
return (
<Table.Summary.Cell key={company.id} index={index + 1} align='right'>
<div>
<Text strong>{chessTableCellFormatValue(String(colTotal), units, decimals)}</Text>
</div>
</Table.Summary.Cell>
);
})}
<Table.Summary.Cell index={data.companies.length + 1} align='right'>
<div style={{ backgroundColor: '#e6f4ea', width: '100%', height: '100%' }}>
<Text strong>{chessTableCellFormatValue(summaryTotal, units, decimals)}</Text>
</div>
</Table.Summary.Cell>
</Table.Summary.Row>
</Table.Summary>
);
}}
/>
);
};

View File

@ -4,6 +4,7 @@ import type React from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../types/chess/chess'; import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../types/chess/chess';
import './ConsolidatedChessTable.css'; import './ConsolidatedChessTable.css';
import { chessTableCellFormatValue } from '../../utils/chessTableCellFormatValue';
const { Text } = Typography; const { Text } = Typography;
@ -36,55 +37,6 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
return result; return result;
}, [data?.cells]); }, [data?.cells]);
const formatValue = (val: string | number | null | undefined) => {
if (val === undefined || val === null || val === '') return '';
let strVal = String(val);
if (strVal.startsWith('(') && strVal.endsWith(')')) {
strVal = `-${strVal.slice(1, -1)}`;
}
if (units !== 'rub') {
let shift = 0;
if (units === 'thousand') shift = 3;
else if (units === 'million') shift = 6;
else if (units === 'billion') shift = 9;
if (shift > 0) {
const isNegative = strVal.startsWith('-');
const clean = isNegative ? strVal.slice(1) : strVal;
const [intPart = '0', fracPart = ''] = clean.split('.');
let newInt = intPart;
let newFrac = fracPart;
if (intPart.length <= shift) {
const zeros = '0'.repeat(shift - intPart.length);
newInt = '0';
newFrac = `${zeros}${intPart}${fracPart}`;
} else {
newInt = intPart.slice(0, -shift);
newFrac = `${intPart.slice(-shift)}${fracPart}`;
}
strVal = `${isNegative ? '-' : ''}${newInt}.${newFrac.slice(0, 4)}`;
}
}
const num = Number(strVal);
if (Number.isNaN(num)) {
return strVal;
}
return new Intl.NumberFormat('ru-RU', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(num);
};
const parseAmount = (val: string | null | undefined): number => { const parseAmount = (val: string | null | undefined): number => {
if (!val) return 0; if (!val) return 0;
@ -109,7 +61,6 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
key: 'name', key: 'name',
fixed: 'left', fixed: 'left',
width: 200, width: 200,
}; };
const companyColumns = data.companies.map((company) => ({ const companyColumns = data.companies.map((company) => ({
@ -119,7 +70,6 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
width: 150, width: 150,
align: 'right' as const, align: 'right' as const,
onCell: (row: TableRow) => { onCell: (row: TableRow) => {
const cell = matrixData[row.id]?.[company.id]; const cell = matrixData[row.id]?.[company.id];
if (!cell) return { className: 'empty-cell' }; if (!cell) return { className: 'empty-cell' };
return {}; return {};
@ -129,9 +79,11 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
if (!cell) return <span style={{ color: '#bfbfbf' }}>—</span>; if (!cell) return <span style={{ color: '#bfbfbf' }}>—</span>;
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 13 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 13 }}>
<span>{formatValue(cell.amount_a)}</span> <span>{chessTableCellFormatValue(cell.amount_a, units, decimals)}</span>
<span>{formatValue(cell.amount_b)}</span> <span>{chessTableCellFormatValue(cell.amount_b, units, decimals)}</span>
<span style={{ color: cell.exceeds_threshold ? 'red' : 'inherit' }}>{formatValue(cell.variance_amount)}</span> <span style={{ color: cell.exceeds_threshold ? 'red' : 'inherit' }}>
{chessTableCellFormatValue(cell.variance_amount, units, decimals)}
</span>
</div> </div>
); );
}, },
@ -165,7 +117,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
return totalRow as TableRow; return totalRow as TableRow;
}, [data?.companies, matrixData]); }, [data?.companies, matrixData]);
const scrollY = isFullscreen ? 'calc(100vh - 180px)' : 436; const scrollY = isFullscreen ? 'calc(100vh - 150px)' : 436;
return ( return (
<Table<TableRow> <Table<TableRow>
@ -175,7 +127,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
pagination={false} pagination={false}
loading={loading} loading={loading}
scroll={{ x: 'max-content', y: scrollY }} scroll={{ x: 'max-content', y: scrollY }}
size='middle' size='small'
bordered bordered
title={() => toolbar} title={() => toolbar}
summary={() => { summary={() => {
@ -193,7 +145,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
{data?.companies.map((company, index) => ( {data?.companies.map((company, index) => (
<Table.Summary.Cell className='resRow' key={company.id} index={index + 1} align='right'> <Table.Summary.Cell className='resRow' key={company.id} index={index + 1} align='right'>
<div> <div>
<Text strong>{formatValue((totalRowData as any)[company.id])}</Text> <Text strong>{chessTableCellFormatValue((totalRowData as any)[company.id], units, decimals)}</Text>
</div> </div>
</Table.Summary.Cell> </Table.Summary.Cell>
))} ))}

View File

@ -3,24 +3,18 @@ import type React from 'react';
import { useEffect, useState } from 'react'; 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 } from '../../types/api';
import type { MatrixCompany } from '../../types/chess/chess'; import type { MatrixCompany } from '../../types/chess/chess';
import { useNotificationApi } from '../../utils/useNotification'; import { useNotificationApi } from '../../utils/useNotification';
import { Account } from '../../types/api';
interface ConsolidationChessTableFilterI { interface ConsolidationChessTableFilterI {
companies: string[]; companies: string[];
onCompaniesChange: (val: string[]) => void; onCompaniesChange: (val: string[]) => void;
decimals: number;
onDecimalsChange: (val: number) => void;
debit: string[]; debit: string[];
onDebitChange: (val: string[]) => void; onDebitChange: (val: string[]) => void;
credit: string[]; credit: string[];
onCreditChange: (val: string[]) => void; onCreditChange: (val: string[]) => void;
allCompanies: MatrixCompany[]; allCompanies: MatrixCompany[];
units: string;
onUnitsChange: (val: string) => void;
} }
const buildTree = (items: Account[]): OptionType[] => { const buildTree = (items: Account[]): OptionType[] => {
@ -59,15 +53,11 @@ const buildTree = (items: Account[]): OptionType[] => {
export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilterI> = ({ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilterI> = ({
companies, companies,
onCompaniesChange, onCompaniesChange,
decimals,
onDecimalsChange,
debit, debit,
onDebitChange, onDebitChange,
credit, credit,
onCreditChange, onCreditChange,
allCompanies, allCompanies,
units,
onUnitsChange,
}) => { }) => {
const [companyOpen, setCompanyOpen] = useState(false); const [companyOpen, setCompanyOpen] = useState(false);
const [debitOpen, setDebitOpen] = useState(false); const [debitOpen, setDebitOpen] = useState(false);
@ -82,18 +72,6 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
const companyOptions = allCompanies.map((c) => ({ label: c.name, value: c.id })); const companyOptions = allCompanies.map((c) => ({ label: c.name, value: c.id }));
const companyTreeData = allCompanies.map((c) => ({ title: c.name, key: c.id })); const companyTreeData = allCompanies.map((c) => ({ title: c.name, key: c.id }));
const UNIT_OPTIONS = [
{ value: 'thousand', label: 'тыс. руб.' },
{ value: 'million', label: 'млн. руб.' },
{ value: 'billion', label: 'млрд руб.' },
];
const DECIMALS_OPTIONS = [
{ value: 0, label: '0' },
{ value: 1, label: '1' },
{ value: 2, label: '2' },
{ value: 3, label: '3' },
];
async function getOptionHandler() { async function getOptionHandler() {
setOptionsLoading(true); setOptionsLoading(true);
try { try {
@ -136,120 +114,101 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
}, []); }, []);
return ( return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 16 }}> <Space size='middle' wrap>
<Space size='middle' wrap> <Space orientation='vertical' size={0}>
<Space orientation='vertical' size={0}> <span style={{ fontSize: 12, color: '#667085' }}>Год</span>
<span style={{ fontSize: 12, color: '#667085' }}>Год</span> <Select disabled style={{ minWidth: 150 }} value={null} options={[]} variant='underlined' />
<Select disabled style={{ minWidth: 150 }} value={null} options={[]} variant='underlined' />
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Квартал</span>
<Select disabled style={{ minWidth: 150 }} value={null} options={[]} variant='underlined' />
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Компании</span>
<Select
mode='multiple'
style={{ minWidth: 200 }}
placeholder='Все компании'
options={companyOptions}
value={companies}
onChange={onCompaniesChange}
maxTagCount='responsive'
open={companyOpen}
onOpenChange={setCompanyOpen}
variant='underlined'
showSearch={false}
popupMatchSelectWidth={false}
popupRender={() => (
<TreeFilterDropdown
title='Компании'
placeholder='Введите название компании'
treeData={companyTreeData}
selectedKeys={companies}
onChange={onCompaniesChange}
onClose={() => setCompanyOpen(false)}
/>
)}
/>
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Дт</span>
<Select
mode='multiple'
style={{ minWidth: 150 }}
placeholder='Все счета'
value={debit}
onChange={onDebitChange}
maxTagCount='responsive'
variant='underlined'
open={debitOpen}
onOpenChange={setDebitOpen}
showSearch={false}
popupMatchSelectWidth={false}
options={flattenTree(debitTreeData)}
popupRender={() => (
<TreeFilterDropdown
title='Дебетовые счета'
placeholder='Введите номер или название счета'
treeData={debitTreeData}
selectedKeys={debit}
onChange={onDebitChange}
onClose={() => setDebitOpen(false)}
/>
)}
loading={optionsLoading}
/>
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Кт</span>
<Select
mode='multiple'
style={{ minWidth: 150 }}
placeholder='Все счета'
value={credit}
onChange={onCreditChange}
maxTagCount='responsive'
variant='underlined'
open={creditOpen}
onOpenChange={setCreditOpen}
showSearch={false}
popupMatchSelectWidth={false}
options={flattenTree(creditTreeData)}
popupRender={() => (
<TreeFilterDropdown
title='Кредитовые счета'
placeholder='Введите номер или название счета'
treeData={creditTreeData}
selectedKeys={credit}
onChange={onCreditChange}
onClose={() => setCreditOpen(false)}
/>
)}
loading={optionsLoading}
/>
</Space>
{companies?.length || debit?.length || credit?.length ? (
<Button onClick={() => dropFiltersHandler()}>Сбросить фильтры</Button>
) : null}
<Space style={{ borderLeft: '1px solid #4754672e', paddingLeft: '8px' }} orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Единицы</span>
<Select style={{ minWidth: 120 }} options={UNIT_OPTIONS} value={units} onChange={onUnitsChange} variant='underlined' />
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Знаков после запятой</span>
<Select
style={{ minWidth: 150 }}
value={decimals}
onChange={(v) => onDecimalsChange(v || 0)}
options={DECIMALS_OPTIONS}
variant='underlined'
/>
</Space>
</Space> </Space>
</div> <Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Квартал</span>
<Select disabled style={{ minWidth: 150 }} value={null} options={[]} variant='underlined' />
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Компании</span>
<Select
mode='multiple'
style={{ minWidth: 200 }}
placeholder='Все компании'
options={companyOptions}
value={companies}
onChange={onCompaniesChange}
maxTagCount='responsive'
open={companyOpen}
onOpenChange={setCompanyOpen}
variant='underlined'
showSearch={false}
popupMatchSelectWidth={false}
popupRender={() => (
<TreeFilterDropdown
title='Компании'
placeholder='Введите название компании'
treeData={companyTreeData}
selectedKeys={companies}
onChange={onCompaniesChange}
onClose={() => setCompanyOpen(false)}
/>
)}
/>
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Дт</span>
<Select
mode='multiple'
style={{ minWidth: 150 }}
placeholder='Все счета'
value={debit}
onChange={onDebitChange}
maxTagCount='responsive'
variant='underlined'
open={debitOpen}
onOpenChange={setDebitOpen}
showSearch={false}
popupMatchSelectWidth={false}
options={flattenTree(debitTreeData)}
popupRender={() => (
<TreeFilterDropdown
title='Дебетовые счета'
placeholder='Введите номер или название счета'
treeData={debitTreeData}
selectedKeys={debit}
onChange={onDebitChange}
onClose={() => setDebitOpen(false)}
/>
)}
loading={optionsLoading}
/>
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Кт</span>
<Select
mode='multiple'
style={{ minWidth: 150 }}
placeholder='Все счета'
value={credit}
onChange={onCreditChange}
maxTagCount='responsive'
variant='underlined'
open={creditOpen}
onOpenChange={setCreditOpen}
showSearch={false}
popupMatchSelectWidth={false}
options={flattenTree(creditTreeData)}
popupRender={() => (
<TreeFilterDropdown
title='Кредитовые счета'
placeholder='Введите номер или название счета'
treeData={creditTreeData}
selectedKeys={credit}
onChange={onCreditChange}
onClose={() => setCreditOpen(false)}
/>
)}
loading={optionsLoading}
/>
</Space>
{companies?.length || debit?.length || credit?.length ? <Button onClick={() => dropFiltersHandler()}>Сбросить фильтры</Button> : null}
</Space>
); );
}; };