diff --git a/web/src/layouts/ConsolidationReportPage.tsx b/web/src/layouts/ConsolidationReportPage.tsx
index 512a3eb..02d72b4 100644
--- a/web/src/layouts/ConsolidationReportPage.tsx
+++ b/web/src/layouts/ConsolidationReportPage.tsx
@@ -60,21 +60,23 @@ function ReportContent() {
}, [tab, navigation, pathname]);
return (
-
- Статус отчёта
-
-
- )
- }
- />
-
+
+
+ Статус отчёта
+
+
+ )
+ }
+ />
+
+
{ActiveTab}
);
diff --git a/web/src/services/consodilation/intercompanyMatrix.ts b/web/src/services/consodilation/intercompanyMatrix.ts
index 4ee2f1f..78702ca 100644
--- a/web/src/services/consodilation/intercompanyMatrix.ts
+++ b/web/src/services/consodilation/intercompanyMatrix.ts
@@ -1,5 +1,5 @@
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) =>
api.get(`/consolidations/versions/${id}/intercompany-matrix/all`);
@@ -20,4 +20,4 @@ export const exportChessTableManual = (versionId: string, data: ChessManualReque
});
export const fetchChessConsolidationsResult = (id: string) =>
- api.get(`/consolidations/versions/${id}/intercompany-matrix/result-table`);
+ api.get(`/consolidations/versions/${id}/intercompany-matrix/result-table`);
diff --git a/web/src/utils/chessTableCellFormatValue.ts b/web/src/utils/chessTableCellFormatValue.ts
new file mode 100644
index 0000000..7c27739
--- /dev/null
+++ b/web/src/utils/chessTableCellFormatValue.ts
@@ -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);
+};
diff --git a/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx b/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx
index f2a60f6..61a23d3 100644
--- a/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx
+++ b/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx
@@ -8,14 +8,16 @@ import {
exportChessTableManual,
fetchChessConsolidations,
fetchChessConsolidationsManual,
+ fetchChessConsolidationsResult,
} 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 { useNotificationApi } from '../../utils/useNotification';
+import { ConsolidationChessFormatTableFilter } from '../ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter';
+import { ConsolidationChessResTable } from '../ConsolidationChessResTable/ConsolidationChessResTable';
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable';
import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter';
-import { VersionCard } from '../../types/consolidations/version_members';
-
export const ChessContent = ({ card }: { card: VersionCard | null }) => {
const { version_id } = useParams<{ version_id: string }>();
@@ -29,26 +31,48 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
const [units, setUnits] = useState('thousand');
const [debit, setDebit] = useState([]);
const [credit, setCredit] = useState([]);
-
- const containerRef = useRef(null);
- const [isFullscreen, setIsFullscreen] = useState(false);
- const toggleFullscreen = async () => {
- if (!containerRef.current) return;
+ const [chessResData, setChessResData] = useState(null);
+ const [chessResUnits, setChessResUnits] = useState('thousand');
+ const [chessResDecimals, setChessResDecimals] = useState(2);
+ const [chessResLoading, setChessResLoading] = useState(false);
+
+ const chessTableRef = useRef(null);
+ const resTableRef = useRef(null);
+ const [isChessFullscreen, setIsChessFullscreen] = useState(false);
+ const [isResFullscreen, setIsResFullscreen] = useState(false);
+
+ const toggleFullscreen = async (ref: React.RefObject, setFullscreen: (val: boolean) => void) => {
+ if (!ref.current) return;
try {
if (!document.fullscreenElement) {
- await containerRef.current.requestFullscreen();
+ await ref.current.requestFullscreen();
+ setFullscreen(true);
} else {
await document.exitFullscreen();
+ setFullscreen(false);
}
} 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) => {
if ((dIds.length > 0 && cIds.length === 0) || (dIds.length === 0 && cIds.length > 0)) {
- notification.info({description:'Для сравнений по Дт и Кт нужно заполнить оба фильтра'})
+ notification.info({ description: 'Для сравнений по Дт и Кт нужно заполнить оба фильтра' });
return;
}
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 () => {
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 ?? ''));
} else {
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) {
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(() => {
if (!version_id) return;
@@ -109,77 +156,132 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
}, [version_id, debit, credit]);
useEffect(() => {
- const handleFullscreenChange = () => {
- setIsFullscreen(!!document.fullscreenElement);
- };
- 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]);
+ if (!version_id) return;
+ loadResData(version_id);
+ }, [version_id]);
return (
- containerRef.current || document.body}>
+ (document.fullscreenElement as HTMLElement) || document.body}>
-
-
-
+
+
+
+
+
+
+
-
-
:
}>
- {isFullscreen ? 'Свернуть' : 'На весь экран'}
-
-
- }
- />
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
);
diff --git a/web/src/widgets/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter.tsx b/web/src/widgets/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter.tsx
new file mode 100644
index 0000000..4497794
--- /dev/null
+++ b/web/src/widgets/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter.tsx
@@ -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 = ({
+ 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 (
+
+
+
+ Единицы
+
+
+
+
+ Знаков после запятой
+
+
+
+ );
+};
diff --git a/web/src/widgets/ConsolidationChessResTable/ConsolidatedChessResTable.css b/web/src/widgets/ConsolidationChessResTable/ConsolidatedChessResTable.css
new file mode 100644
index 0000000..6adec5f
--- /dev/null
+++ b/web/src/widgets/ConsolidationChessResTable/ConsolidatedChessResTable.css
@@ -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 ;
+}
diff --git a/web/src/widgets/ConsolidationChessResTable/ConsolidationChessResTable.tsx b/web/src/widgets/ConsolidationChessResTable/ConsolidationChessResTable.tsx
new file mode 100644
index 0000000..1bd77b1
--- /dev/null
+++ b/web/src/widgets/ConsolidationChessResTable/ConsolidationChessResTable.tsx
@@ -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 = ({
+ data,
+ loading,
+ decimals,
+ units,
+ toolbar,
+ isFullscreen = false,
+}) => {
+ const matrixData = useMemo(() => {
+ if (!data?.articles) return {};
+ const result: Record> = {};
+
+ 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 = useMemo(() => {
+ if (!data?.companies) return [];
+
+ const firstColumn: ColumnsType[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 —;
+ }
+ return formatted;
+ },
+ }));
+
+ const totalColumn: ColumnsType[number] = {
+ title: 'Итого',
+ dataIndex: 'group_total',
+ key: 'group_total',
+ width: 150,
+ align: 'right',
+ render: (val: string) => {chessTableCellFormatValue(val, units, decimals)},
+ };
+
+ 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 (
+
+ 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 (
+
+
+
+
+ Итого
+
+
+
+ {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 (
+
+
+ {chessTableCellFormatValue(String(colTotal), units, decimals)}
+
+
+ );
+ })}
+
+
+
+ {chessTableCellFormatValue(summaryTotal, units, decimals)}
+
+
+
+
+ );
+ }}
+ />
+ );
+};
diff --git a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx b/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx
index 5a226dd..1f3e8d4 100644
--- a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx
+++ b/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx
@@ -4,6 +4,7 @@ import type React from 'react';
import { useMemo } from 'react';
import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../types/chess/chess';
import './ConsolidatedChessTable.css';
+import { chessTableCellFormatValue } from '../../utils/chessTableCellFormatValue';
const { Text } = Typography;
@@ -36,55 +37,6 @@ export const ConsolidationChessTable: React.FC = ({
return result;
}, [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 => {
if (!val) return 0;
@@ -109,7 +61,6 @@ export const ConsolidationChessTable: React.FC = ({
key: 'name',
fixed: 'left',
width: 200,
-
};
const companyColumns = data.companies.map((company) => ({
@@ -119,7 +70,6 @@ export const ConsolidationChessTable: React.FC = ({
width: 150,
align: 'right' as const,
onCell: (row: TableRow) => {
-
const cell = matrixData[row.id]?.[company.id];
if (!cell) return { className: 'empty-cell' };
return {};
@@ -129,9 +79,11 @@ export const ConsolidationChessTable: React.FC = ({
if (!cell) return —;
return (
- {formatValue(cell.amount_a)}
- {formatValue(cell.amount_b)}
- {formatValue(cell.variance_amount)}
+ {chessTableCellFormatValue(cell.amount_a, units, decimals)}
+ {chessTableCellFormatValue(cell.amount_b, units, decimals)}
+
+ {chessTableCellFormatValue(cell.variance_amount, units, decimals)}
+
);
},
@@ -165,7 +117,7 @@ export const ConsolidationChessTable: React.FC = ({
return totalRow as TableRow;
}, [data?.companies, matrixData]);
- const scrollY = isFullscreen ? 'calc(100vh - 180px)' : 436;
+ const scrollY = isFullscreen ? 'calc(100vh - 150px)' : 436;
return (
@@ -175,7 +127,7 @@ export const ConsolidationChessTable: React.FC = ({
pagination={false}
loading={loading}
scroll={{ x: 'max-content', y: scrollY }}
- size='middle'
+ size='small'
bordered
title={() => toolbar}
summary={() => {
@@ -193,7 +145,7 @@ export const ConsolidationChessTable: React.FC = ({
{data?.companies.map((company, index) => (
- {formatValue((totalRowData as any)[company.id])}
+ {chessTableCellFormatValue((totalRowData as any)[company.id], units, decimals)}
))}
diff --git a/web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx b/web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
index f60f706..54c82bf 100644
--- a/web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
+++ b/web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx
@@ -3,24 +3,18 @@ import type React from 'react';
import { useEffect, useState } from 'react';
import { ApiError, api } from '../../api/client';
import { type OptionType, TreeFilterDropdown } from '../../components/TreeFilterDropdown/TreeFilterDropdown';
+import type { Account } from '../../types/api';
import type { MatrixCompany } from '../../types/chess/chess';
import { useNotificationApi } from '../../utils/useNotification';
-import { Account } from '../../types/api';
-
-
interface ConsolidationChessTableFilterI {
companies: string[];
onCompaniesChange: (val: string[]) => void;
- decimals: number;
- onDecimalsChange: (val: number) => void;
debit: string[];
onDebitChange: (val: string[]) => void;
credit: string[];
onCreditChange: (val: string[]) => void;
allCompanies: MatrixCompany[];
- units: string;
- onUnitsChange: (val: string) => void;
}
const buildTree = (items: Account[]): OptionType[] => {
@@ -59,15 +53,11 @@ const buildTree = (items: Account[]): OptionType[] => {
export const ConsolidationChessTableFilter: React.FC = ({
companies,
onCompaniesChange,
- decimals,
- onDecimalsChange,
debit,
onDebitChange,
credit,
onCreditChange,
allCompanies,
- units,
- onUnitsChange,
}) => {
const [companyOpen, setCompanyOpen] = useState(false);
const [debitOpen, setDebitOpen] = useState(false);
@@ -82,18 +72,6 @@ export const ConsolidationChessTableFilter: React.FC ({ label: c.name, value: 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() {
setOptionsLoading(true);
try {
@@ -136,120 +114,101 @@ export const ConsolidationChessTableFilter: React.FC
-
-
- Год
-
-
-
- Квартал
-
-
-
- Компании
-
-
-
- Дт
-
-
-
- Кт
-
- {companies?.length || debit?.length || credit?.length ? (
-
- ) : null}
-
- Единицы
-
-
-
-
- Знаков после запятой
-
+
+
+ Год
+
-
+
+ Квартал
+
+
+
+ Компании
+
+
+
+ Дт
+
+
+
+ Кт
+
+ {companies?.length || debit?.length || credit?.length ? : null}
+
);
};