From cbeb9daa384f2f0ae7a80bd9a97e33abc2a23e37 Mon Sep 17 00:00:00 2001 From: Grog Date: Wed, 21 Oct 2026 16:48:11 +0300 Subject: [PATCH] fixed bugs --- web/src/App.tsx | 4 +- .../TreeFilterDropdown/TreeFilterDropdown.tsx | 29 +++- ...rtPage.tsx => ConsolidationReportPage.tsx} | 20 ++- web/src/layouts/index.ts | 2 +- .../{chess.ts => intercompanyMatrix.ts} | 3 + web/src/types/api.ts | 12 ++ web/src/types/chess/chess.ts | 14 ++ .../ConsolidatedChessContent.tsx | 144 ++++++++++-------- .../ConsolidationChessTable.tsx | 68 ++++++--- .../ConsolidationChessTableFilter.tsx | 27 ++-- 10 files changed, 214 insertions(+), 109 deletions(-) rename web/src/layouts/{ConsolidatioReportPage.tsx => ConsolidationReportPage.tsx} (83%) rename web/src/services/consodilation/{chess.ts => intercompanyMatrix.ts} (85%) rename web/src/widgets/{ConsolidationChessTable => ConsolidationChessTableFilter}/ConsolidationChessTableFilter.tsx (93%) diff --git a/web/src/App.tsx b/web/src/App.tsx index 3d972ee..871c730 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -17,7 +17,7 @@ import { Main, Mono, } from './layouts/'; -import { ConsolidatioReportPage } from './layouts/ConsolidatioReportPage'; +import { ConsolidationReportPage } from './layouts/ConsolidationReportPage'; import { store } from './store/store'; import { Sidebar } from './widgets/Sidebar/Sidebar'; @@ -37,7 +37,7 @@ function App() { } /> } /> - } /> + } /> } /> } /> diff --git a/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx b/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx index b33a829..1c313a0 100644 --- a/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx +++ b/web/src/components/TreeFilterDropdown/TreeFilterDropdown.tsx @@ -80,6 +80,9 @@ export const TreeFilterDropdown: React.FC = ({ return keys; }; + const allFilteredKeys = getAllNodeKeys(filteredTreeData); + const visibleCheckedKeys = checkedKeys.filter((key) => allFilteredKeys.includes(key)); + const handleSelectAll = () => { setCheckedKeys(getAllNodeKeys(treeData)); }; @@ -116,10 +119,28 @@ export const TreeFilterDropdown: React.FC = ({ checkable selectable={false} treeData={filteredTreeData} - checkedKeys={checkedKeys} - onCheck={(keys) => { - const checked = Array.isArray(keys) ? keys : keys.checked; - setCheckedKeys(checked as string[]); + checkedKeys={visibleCheckedKeys} + onCheck={(keys, e) => { + const newChecked = Array.isArray(keys) ? keys : keys.checked; + const targetKey = e.node.key as string; + const isChecked = newChecked.includes(targetKey); + + if (isChecked) { + setCheckedKeys(Array.from(new Set([...checkedKeys, ...newChecked.map(String)]))); + } else { + const keysToRemove = [targetKey]; + const findChildren = (node: OptionType) => { + if (node.children) { + node.children.forEach((child) => { + keysToRemove.push(child.key); + findChildren(child); + }); + } + }; + findChildren(e.node); + + setCheckedKeys(checkedKeys.filter((k) => !keysToRemove.includes(k))); + } }} expandedKeys={expandedKeys} onExpand={(keys) => setExpandedKeys(keys as string[])} diff --git a/web/src/layouts/ConsolidatioReportPage.tsx b/web/src/layouts/ConsolidationReportPage.tsx similarity index 83% rename from web/src/layouts/ConsolidatioReportPage.tsx rename to web/src/layouts/ConsolidationReportPage.tsx index 19693f6..512a3eb 100644 --- a/web/src/layouts/ConsolidatioReportPage.tsx +++ b/web/src/layouts/ConsolidationReportPage.tsx @@ -1,6 +1,6 @@ import { Space, message } from 'antd'; import { type ReactNode, useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { ApiError } from '../api/client'; import PageTitle from '../components/PageTitle'; import { TableTag } from '../components/TableTag/TableTag'; @@ -17,11 +17,12 @@ import { OsvContent } from '../widgets/ConsolidatedOsvContent/ConsolidatedOsvCon function ReportContent() { const [card, setCard] = useState(null); const { version_id, tab } = useParams<{ tab?: string; version_id: string }>(); - + const navigation = useNavigate(); + const { pathname } = useLocation(); const TAB_COMPONENTS: Record = { osv: , - chess: , + chess: , }; const ActiveTab = TAB_COMPONENTS[tab as string] ?? null; @@ -46,6 +47,17 @@ function ReportContent() { }; } }, [version_id]); + + useEffect(() => { + const isValidTab = !tab || tab === 'osv' || tab === 'chess'; + + if (!isValidTab) { + const pathParts = pathname.split('/'); + pathParts[pathParts.length - 1] = 'osv'; + const newPath = pathParts.join('/'); + navigation(newPath, { replace: true }); + } + }, [tab, navigation, pathname]); return (
(); // key: при смене версии всё состояние (карточка, фильтры, страница, данные) создаётся заново return ; diff --git a/web/src/layouts/index.ts b/web/src/layouts/index.ts index 90015f7..33cee41 100644 --- a/web/src/layouts/index.ts +++ b/web/src/layouts/index.ts @@ -9,4 +9,4 @@ export { GroupsLayout } from './Groups'; export { AuditLogging } from './AuditLogging'; export { Admin } from './Admin'; export { DocumentPage } from './DocumentPage'; -export { ConsolidatioReportPage } from './ConsolidatioReportPage'; +export { ConsolidationReportPage } from './ConsolidationReportPage'; diff --git a/web/src/services/consodilation/chess.ts b/web/src/services/consodilation/intercompanyMatrix.ts similarity index 85% rename from web/src/services/consodilation/chess.ts rename to web/src/services/consodilation/intercompanyMatrix.ts index 5ecf609..4ee2f1f 100644 --- a/web/src/services/consodilation/chess.ts +++ b/web/src/services/consodilation/intercompanyMatrix.ts @@ -18,3 +18,6 @@ export const exportChessTableManual = (versionId: string, data: ChessManualReque }, body: JSON.stringify(data), }); + +export const fetchChessConsolidationsResult = (id: string) => + api.get(`/consolidations/versions/${id}/intercompany-matrix/result-table`); diff --git a/web/src/types/api.ts b/web/src/types/api.ts index c6e0850..cf2c7c9 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -394,3 +394,15 @@ export interface LegalEntityBrief { name: string; inn: string; } + +export interface Account { + id: string; + code: string; + parent_id: string | null; + name: string; + kind: string; + row_type: string; + is_off_balance: boolean; + is_debit: boolean; + is_credit: boolean; +} diff --git a/web/src/types/chess/chess.ts b/web/src/types/chess/chess.ts index 67e4d15..7da93a6 100644 --- a/web/src/types/chess/chess.ts +++ b/web/src/types/chess/chess.ts @@ -2,6 +2,10 @@ export interface MatrixCompany { id: string; name: string; } +export interface MatrixAmount { + company_id: string; + amount: string; +} export interface MatrixCell { company_a_id: string; @@ -15,10 +19,20 @@ export interface MatrixCell { skipped_osv_entry_count: number; } +export interface MatrixResCell { + code: string; + name: string; + group_total: string; + amounts: MatrixAmount[]; +} export interface IntercompanyMatrixResponse { companies: MatrixCompany[]; cells: MatrixCell[]; } +export interface IntercompanyMatrixResultResponse { + companies: MatrixCompany[]; + articles: MatrixResCell[]; +} export type ChessManualRequest = { debit_account_ids: string[]; diff --git a/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx b/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx index fc076a4..f2a60f6 100644 --- a/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx +++ b/web/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx @@ -1,5 +1,5 @@ import { DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons'; -import { Button } from 'antd'; +import { Button, ConfigProvider } from 'antd'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { ApiError } from '../../api/client'; @@ -8,14 +8,16 @@ import { exportChessTableManual, fetchChessConsolidations, fetchChessConsolidationsManual, -} from '../../services/consodilation/chess'; +} from '../../services/consodilation/intercompanyMatrix'; import type { IntercompanyMatrixResponse } from '../../types/chess/chess'; -import { saveBlob } from '../../utils/download'; +import { exportFilename, saveBlob } from '../../utils/download'; import { useNotificationApi } from '../../utils/useNotification'; import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable'; -import { ConsolidationChessTableFilter } from '../ConsolidationChessTable/ConsolidationChessTableFilter'; +import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter'; +import { VersionCard } from '../../types/consolidations/version_members'; -export const ChessContent = () => { + +export const ChessContent = ({ card }: { card: VersionCard | null }) => { const { version_id } = useParams<{ version_id: string }>(); const notification = useNotificationApi(); @@ -27,7 +29,7 @@ export const ChessContent = () => { const [units, setUnits] = useState('thousand'); const [debit, setDebit] = useState([]); const [credit, setCredit] = useState([]); - + const containerRef = useRef(null); const [isFullscreen, setIsFullscreen] = useState(false); @@ -44,7 +46,11 @@ export const ChessContent = () => { } }; - const loadData = async (vId: string, dIds: string[], cIds: string[]) => { + 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:'Для сравнений по Дт и Кт нужно заполнить оба фильтра'}) + return; + } setLoading(true); try { let res: IntercompanyMatrixResponse; @@ -53,13 +59,17 @@ export const ChessContent = () => { } else { res = await fetchChessConsolidations(vId); } - setData(res); + if (isActual()) { + setData(res); + } } catch (e) { - if (e instanceof ApiError) { + if (isActual() && e instanceof ApiError) { notification.error({ description: e.message }); } } finally { - setLoading(false); + if (isActual()) { + setLoading(false); + } } }; @@ -71,7 +81,7 @@ export const ChessContent = () => { try { if (debit?.length && credit?.length) { const { blob, filename } = await exportChessTableManual(version_id, { debit_account_ids: debit, credit_account_ids: credit }); - saveBlob(blob, filename ?? 'Шахматка'); + saveBlob(blob, filename ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? '')); } else { const { blob, filename } = await exportChessTableAll(version_id); saveBlob(blob, filename ?? 'Шахматка'); @@ -87,7 +97,15 @@ export const ChessContent = () => { useEffect(() => { if (!version_id) return; - loadData(version_id, debit, credit); + + let isMounted = true; + const isActual = () => isMounted; + + loadData(version_id, debit, credit, isActual); + + return () => { + isMounted = false; + }; }, [version_id, debit, credit]); useEffect(() => { @@ -111,56 +129,58 @@ export const ChessContent = () => { }, [data, companies]); return ( -
- -
- + containerRef.current || document.body}> +
+ +
+ +
+ +
- - -
- } - /> -
+ } + /> +
+ ); }; diff --git a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx b/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx index d380f64..5a226dd 100644 --- a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx +++ b/web/src/widgets/ConsolidationChessTable/ConsolidationChessTable.tsx @@ -36,20 +36,53 @@ export const ConsolidationChessTable: React.FC = ({ return result; }, [data?.cells]); - const formatValue = (val: string | number) => { + const formatValue = (val: string | number | null | undefined) => { if (val === undefined || val === null || val === '') return ''; - let num = typeof val === 'string' ? Number.parseFloat(val) : val; - if (Number.isNaN(num)) return String(val); - if (units === 'thousand') { - num /= 1000; - } else if (units === 'million') { - num /= 1000000; - } else if (units === 'billion') { - num /= 1_000_000_000; + let strVal = String(val); + + if (strVal.startsWith('(') && strVal.endsWith(')')) { + strVal = `-${strVal.slice(1, -1)}`; } - return num.toFixed(decimals).replace('.', ','); + 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 => { @@ -58,7 +91,7 @@ export const ConsolidationChessTable: React.FC = ({ const isNegative: boolean = val.startsWith('(') && val.endsWith(')'); const cleanVal = isNegative ? val.slice(1, -1) : val; - const num = Number.parseFloat(cleanVal); + const num = Number(cleanVal); if (Number.isNaN(num)) { return 0; @@ -76,13 +109,7 @@ export const ConsolidationChessTable: React.FC = ({ key: 'name', fixed: 'left', width: 200, - render: (text, row) => {text}, - onCell: (row) => ({ - style: { - background: row.isTotal ? '#e6f4ea' : '#fff', - zIndex: 2, - }, - }), + }; const companyColumns = data.companies.map((company) => ({ @@ -92,17 +119,12 @@ export const ConsolidationChessTable: React.FC = ({ width: 150, align: 'right' as const, onCell: (row: TableRow) => { - if (row.isTotal) return { style: { background: '#e6f4ea' } }; const cell = matrixData[row.id]?.[company.id]; if (!cell) return { className: 'empty-cell' }; return {}; }, render: (_: unknown, row: TableRow) => { - if (row.isTotal) { - const totalVal = (row as any)[company.id]; - return {formatValue(totalVal)}; - } const cell = matrixData[row.id]?.[company.id]; if (!cell) return —; return ( diff --git a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTableFilter.tsx b/web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx similarity index 93% rename from web/src/widgets/ConsolidationChessTable/ConsolidationChessTableFilter.tsx rename to web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx index 2a0f3af..f60f706 100644 --- a/web/src/widgets/ConsolidationChessTable/ConsolidationChessTableFilter.tsx +++ b/web/src/widgets/ConsolidationChessTableFilter/ConsolidationChessTableFilter.tsx @@ -5,18 +5,9 @@ import { ApiError, api } from '../../api/client'; import { type OptionType, TreeFilterDropdown } from '../../components/TreeFilterDropdown/TreeFilterDropdown'; import type { MatrixCompany } from '../../types/chess/chess'; import { useNotificationApi } from '../../utils/useNotification'; +import { Account } from '../../types/api'; + -interface Account { - id: string; - code: string; - parent_id: string | null; - name: string; - kind: string; - row_type: string; - is_off_balance: boolean; - is_debit: boolean; - is_credit: boolean; -} interface ConsolidationChessTableFilterI { companies: string[]; @@ -124,6 +115,16 @@ export const ConsolidationChessTableFilter: React.FC { + let result: { label: string; value: string }[] = []; + nodes.forEach((node) => { + result.push({ label: String(node.code), value: String(node.key) }); + if (node.children) { + result = [...result, ...flattenTree(node.children)]; + } + }); + return result; + }; function dropFiltersHandler() { onCompaniesChange([]); onDebitChange([]); @@ -187,7 +188,7 @@ export const ConsolidationChessTableFilter: React.FC ({ label: node.code, value: node.key }))} + options={flattenTree(debitTreeData)} popupRender={() => ( ({ label: node.code, value: node.key }))} + options={flattenTree(creditTreeData)} popupRender={() => (