From d1c0f93360b705df8c33ec88757be025d6cb7505 Mon Sep 17 00:00:00 2001 From: Grog Date: Thu, 22 Oct 2026 10:07:33 +0300 Subject: [PATCH] =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D1=83=D1=8E=D1=89=D0=B0=D1=8F=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86=D0=B2=20=D0=92=D0=93=D0=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/layouts/ConsolidationReportPage.tsx | 32 +-- .../consodilation/intercompanyMatrix.ts | 4 +- web/src/utils/chessTableCellFormatValue.ts | 44 +++ .../ConsolidatedChessContent.tsx | 252 ++++++++++++------ .../ConsolidationChessFormatTableFilter.tsx | 50 ++++ .../ConsolidatedChessResTable.css | 11 + .../ConsolidationChessResTable.tsx | 148 ++++++++++ .../ConsolidationChessTable.tsx | 66 +---- .../ConsolidationChessTableFilter.tsx | 233 +++++++--------- 9 files changed, 554 insertions(+), 286 deletions(-) create mode 100644 web/src/utils/chessTableCellFormatValue.ts create mode 100644 web/src/widgets/ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter.tsx create mode 100644 web/src/widgets/ConsolidationChessResTable/ConsolidatedChessResTable.css create mode 100644 web/src/widgets/ConsolidationChessResTable/ConsolidationChessResTable.tsx 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}>
- -
- +
+ +
+ +
+ +
+
+ + +
- - -
- } - /> + } + /> +
+ +
+ +
+ +
+
+ +
+
+ } + /> +
); 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 ( +
+ + + Единицы + onDecimalsChange(v || 0)} + options={DECIMALS_OPTIONS} + variant='underlined' + /> + + +
+ ); +}; 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 - - - Год - - - - Компании - ( - setDebitOpen(false)} - /> - )} - loading={optionsLoading} - /> - - - - Кт - - - - - Знаков после запятой - - + + Квартал + ( + setCompanyOpen(false)} + /> + )} + /> + + + + Дт + ( + setCreditOpen(false)} + /> + )} + loading={optionsLoading} + /> + + {companies?.length || debit?.length || credit?.length ? : null} + ); };