fixed bugs

This commit is contained in:
Grog 2026-10-21 16:48:11 +03:00
parent 91fdc6c0f2
commit cbeb9daa38
10 changed files with 214 additions and 109 deletions

View File

@ -17,7 +17,7 @@ import {
Main, Main,
Mono, Mono,
} from './layouts/'; } from './layouts/';
import { ConsolidatioReportPage } from './layouts/ConsolidatioReportPage'; import { ConsolidationReportPage } from './layouts/ConsolidationReportPage';
import { store } from './store/store'; import { store } from './store/store';
import { Sidebar } from './widgets/Sidebar/Sidebar'; import { Sidebar } from './widgets/Sidebar/Sidebar';
@ -37,7 +37,7 @@ function App() {
<Route path='/groups'> <Route path='/groups'>
<Route index element={<GroupsLayout />} /> <Route index element={<GroupsLayout />} />
<Route path=':group_id' element={<ConsolidationPage />} /> <Route path=':group_id' element={<ConsolidationPage />} />
<Route path=':group_id/versions/:version_id/:tab' element={<ConsolidatioReportPage />} /> <Route path=':group_id/versions/:version_id/:tab' element={<ConsolidationReportPage />} />
<Route path='monozaemchiki' element={<Mono />} /> <Route path='monozaemchiki' element={<Mono />} />
</Route> </Route>
<Route path='/catalogue/groups' element={<CatalogueGroup />} /> <Route path='/catalogue/groups' element={<CatalogueGroup />} />

View File

@ -80,6 +80,9 @@ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
return keys; return keys;
}; };
const allFilteredKeys = getAllNodeKeys(filteredTreeData);
const visibleCheckedKeys = checkedKeys.filter((key) => allFilteredKeys.includes(key));
const handleSelectAll = () => { const handleSelectAll = () => {
setCheckedKeys(getAllNodeKeys(treeData)); setCheckedKeys(getAllNodeKeys(treeData));
}; };
@ -116,10 +119,28 @@ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
checkable checkable
selectable={false} selectable={false}
treeData={filteredTreeData} treeData={filteredTreeData}
checkedKeys={checkedKeys} checkedKeys={visibleCheckedKeys}
onCheck={(keys) => { onCheck={(keys, e) => {
const checked = Array.isArray(keys) ? keys : keys.checked; const newChecked = Array.isArray(keys) ? keys : keys.checked;
setCheckedKeys(checked as string[]); 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} expandedKeys={expandedKeys}
onExpand={(keys) => setExpandedKeys(keys as string[])} onExpand={(keys) => setExpandedKeys(keys as string[])}

View File

@ -1,6 +1,6 @@
import { Space, message } from 'antd'; import { Space, message } from 'antd';
import { type ReactNode, useEffect, useState } from 'react'; 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 { ApiError } from '../api/client';
import PageTitle from '../components/PageTitle'; import PageTitle from '../components/PageTitle';
import { TableTag } from '../components/TableTag/TableTag'; import { TableTag } from '../components/TableTag/TableTag';
@ -17,11 +17,12 @@ import { OsvContent } from '../widgets/ConsolidatedOsvContent/ConsolidatedOsvCon
function ReportContent() { function ReportContent() {
const [card, setCard] = useState<VersionCard | null>(null); const [card, setCard] = useState<VersionCard | null>(null);
const { version_id, tab } = useParams<{ tab?: string; version_id: string }>(); const { version_id, tab } = useParams<{ tab?: string; version_id: string }>();
const navigation = useNavigate();
const { pathname } = useLocation();
const TAB_COMPONENTS: Record<string, ReactNode> = { const TAB_COMPONENTS: Record<string, ReactNode> = {
osv: <OsvContent card={card} />, osv: <OsvContent card={card} />,
chess: <ChessContent />, chess: <ChessContent card={card} />,
}; };
const ActiveTab = TAB_COMPONENTS[tab as string] ?? null; const ActiveTab = TAB_COMPONENTS[tab as string] ?? null;
@ -46,6 +47,17 @@ function ReportContent() {
}; };
} }
}, [version_id]); }, [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 ( 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 <PageTitle
@ -68,7 +80,7 @@ function ReportContent() {
); );
} }
export function ConsolidatioReportPage() { export function ConsolidationReportPage() {
const { version_id = '' } = useParams<{ version_id: string }>(); const { version_id = '' } = useParams<{ version_id: string }>();
// key: при смене версии всё состояние (карточка, фильтры, страница, данные) создаётся заново // key: при смене версии всё состояние (карточка, фильтры, страница, данные) создаётся заново
return <ReportContent key={version_id} />; return <ReportContent key={version_id} />;

View File

@ -9,4 +9,4 @@ export { GroupsLayout } from './Groups';
export { AuditLogging } from './AuditLogging'; export { AuditLogging } from './AuditLogging';
export { Admin } from './Admin'; export { Admin } from './Admin';
export { DocumentPage } from './DocumentPage'; export { DocumentPage } from './DocumentPage';
export { ConsolidatioReportPage } from './ConsolidatioReportPage'; export { ConsolidationReportPage } from './ConsolidationReportPage';

View File

@ -18,3 +18,6 @@ export const exportChessTableManual = (versionId: string, data: ChessManualReque
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
export const fetchChessConsolidationsResult = (id: string) =>
api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/result-table`);

View File

@ -394,3 +394,15 @@ export interface LegalEntityBrief {
name: string; name: string;
inn: 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;
}

View File

@ -2,6 +2,10 @@ export interface MatrixCompany {
id: string; id: string;
name: string; name: string;
} }
export interface MatrixAmount {
company_id: string;
amount: string;
}
export interface MatrixCell { export interface MatrixCell {
company_a_id: string; company_a_id: string;
@ -15,10 +19,20 @@ export interface MatrixCell {
skipped_osv_entry_count: number; skipped_osv_entry_count: number;
} }
export interface MatrixResCell {
code: string;
name: string;
group_total: string;
amounts: MatrixAmount[];
}
export interface IntercompanyMatrixResponse { export interface IntercompanyMatrixResponse {
companies: MatrixCompany[]; companies: MatrixCompany[];
cells: MatrixCell[]; cells: MatrixCell[];
} }
export interface IntercompanyMatrixResultResponse {
companies: MatrixCompany[];
articles: MatrixResCell[];
}
export type ChessManualRequest = { export type ChessManualRequest = {
debit_account_ids: string[]; debit_account_ids: string[];

View File

@ -1,5 +1,5 @@
import { DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons'; 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 { useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { ApiError } from '../../api/client'; import { ApiError } from '../../api/client';
@ -8,14 +8,16 @@ import {
exportChessTableManual, exportChessTableManual,
fetchChessConsolidations, fetchChessConsolidations,
fetchChessConsolidationsManual, fetchChessConsolidationsManual,
} from '../../services/consodilation/chess'; } from '../../services/consodilation/intercompanyMatrix';
import type { IntercompanyMatrixResponse } from '../../types/chess/chess'; import type { IntercompanyMatrixResponse } from '../../types/chess/chess';
import { saveBlob } from '../../utils/download'; import { exportFilename, saveBlob } from '../../utils/download';
import { useNotificationApi } from '../../utils/useNotification'; import { useNotificationApi } from '../../utils/useNotification';
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable'; 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 { version_id } = useParams<{ version_id: string }>();
const notification = useNotificationApi(); const notification = useNotificationApi();
@ -27,7 +29,7 @@ export const ChessContent = () => {
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 containerRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false); 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); setLoading(true);
try { try {
let res: IntercompanyMatrixResponse; let res: IntercompanyMatrixResponse;
@ -53,13 +59,17 @@ export const ChessContent = () => {
} else { } else {
res = await fetchChessConsolidations(vId); res = await fetchChessConsolidations(vId);
} }
setData(res); if (isActual()) {
setData(res);
}
} catch (e) { } catch (e) {
if (e instanceof ApiError) { if (isActual() && e instanceof ApiError) {
notification.error({ description: e.message }); notification.error({ description: e.message });
} }
} finally { } finally {
setLoading(false); if (isActual()) {
setLoading(false);
}
} }
}; };
@ -71,7 +81,7 @@ export const ChessContent = () => {
try { try {
if (debit?.length && credit?.length) { if (debit?.length && credit?.length) {
const { blob, filename } = await exportChessTableManual(version_id, { debit_account_ids: debit, credit_account_ids: credit }); 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 { } else {
const { blob, filename } = await exportChessTableAll(version_id); const { blob, filename } = await exportChessTableAll(version_id);
saveBlob(blob, filename ?? 'Шахматка'); saveBlob(blob, filename ?? 'Шахматка');
@ -87,7 +97,15 @@ export const ChessContent = () => {
useEffect(() => { useEffect(() => {
if (!version_id) return; 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]); }, [version_id, debit, credit]);
useEffect(() => { useEffect(() => {
@ -111,56 +129,58 @@ export const ChessContent = () => {
}, [data, companies]); }, [data, companies]);
return ( return (
<div <ConfigProvider getPopupContainer={() => containerRef.current || document.body}>
ref={containerRef} <div
style={{ ref={containerRef}
backgroundColor: isFullscreen ? '#fff' : 'transparent', style={{
padding: isFullscreen ? 24 : 0, backgroundColor: isFullscreen ? '#fff' : 'transparent',
display: 'flex', padding: isFullscreen ? 24 : 0,
flexDirection: 'column', display: 'flex',
height: isFullscreen ? '100vh' : 'auto', flexDirection: 'column',
overflow: isFullscreen ? 'hidden' : 'visible', height: isFullscreen ? '100vh' : 'auto',
}}> overflow: isFullscreen ? 'hidden' : 'visible',
<ConsolidationChessTable }}>
data={filteredData} <ConsolidationChessTable
loading={loading} data={filteredData}
decimals={decimals} loading={loading}
units={units} decimals={decimals}
isFullscreen={isFullscreen} units={units}
toolbar={ isFullscreen={isFullscreen}
<div toolbar={
style={{ <div
display: 'flex', style={{
justifyContent: 'space-between', display: 'flex',
alignItems: 'center', justifyContent: 'space-between',
width: '100%', alignItems: 'center',
flexWrap: 'wrap', width: '100%',
gap: 16, flexWrap: 'wrap',
}}> gap: 16,
<div style={{ flex: '1 1 auto' }}> }}>
<ConsolidationChessTableFilter <div style={{ flex: '1 1 auto' }}>
companies={companies} <ConsolidationChessTableFilter
onCompaniesChange={setCompanies} companies={companies}
decimals={decimals} onCompaniesChange={setCompanies}
onDecimalsChange={setDecimals} decimals={decimals}
units={units} onDecimalsChange={setDecimals}
onUnitsChange={setUnits} units={units}
debit={debit} onUnitsChange={setUnits}
onDebitChange={setDebit} debit={debit}
credit={credit} onDebitChange={setDebit}
onCreditChange={setCredit} credit={credit}
allCompanies={data?.companies || []} onCreditChange={setCredit}
/> allCompanies={data?.companies || []}
/>
</div>
<Button loading={expoerLoading} onClick={() => download()} icon={<DownloadOutlined />}>
Экспорт
</Button>
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
{isFullscreen ? 'Свернуть' : 'На весь экран'}
</Button>
</div> </div>
<Button loading={expoerLoading} onClick={() => download()} icon={<DownloadOutlined />}> }
Экспорт />
</Button> </div>
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}> </ConfigProvider>
{isFullscreen ? 'Свернуть' : 'На весь экран'}
</Button>
</div>
}
/>
</div>
); );
}; };

View File

@ -36,20 +36,53 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
return result; return result;
}, [data?.cells]); }, [data?.cells]);
const formatValue = (val: string | number) => { const formatValue = (val: string | number | null | undefined) => {
if (val === undefined || val === null || val === '') return ''; 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') { let strVal = String(val);
num /= 1000;
} else if (units === 'million') { if (strVal.startsWith('(') && strVal.endsWith(')')) {
num /= 1000000; strVal = `-${strVal.slice(1, -1)}`;
} else if (units === 'billion') {
num /= 1_000_000_000;
} }
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 => { const parseAmount = (val: string | null | undefined): number => {
@ -58,7 +91,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
const isNegative: boolean = val.startsWith('(') && val.endsWith(')'); const isNegative: boolean = val.startsWith('(') && val.endsWith(')');
const cleanVal = isNegative ? val.slice(1, -1) : val; const cleanVal = isNegative ? val.slice(1, -1) : val;
const num = Number.parseFloat(cleanVal); const num = Number(cleanVal);
if (Number.isNaN(num)) { if (Number.isNaN(num)) {
return 0; return 0;
@ -76,13 +109,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
key: 'name', key: 'name',
fixed: 'left', fixed: 'left',
width: 200, width: 200,
render: (text, row) => <Text strong={row.isTotal}>{text}</Text>,
onCell: (row) => ({
style: {
background: row.isTotal ? '#e6f4ea' : '#fff',
zIndex: 2,
},
}),
}; };
const companyColumns = data.companies.map((company) => ({ const companyColumns = data.companies.map((company) => ({
@ -92,17 +119,12 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
width: 150, width: 150,
align: 'right' as const, align: 'right' as const,
onCell: (row: TableRow) => { onCell: (row: TableRow) => {
if (row.isTotal) return { style: { background: '#e6f4ea' } };
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 {};
}, },
render: (_: unknown, row: TableRow) => { render: (_: unknown, row: TableRow) => {
if (row.isTotal) {
const totalVal = (row as any)[company.id];
return <Text strong>{formatValue(totalVal)}</Text>;
}
const cell = matrixData[row.id]?.[company.id]; const cell = matrixData[row.id]?.[company.id];
if (!cell) return <span style={{ color: '#bfbfbf' }}>—</span>; if (!cell) return <span style={{ color: '#bfbfbf' }}>—</span>;
return ( return (

View File

@ -5,18 +5,9 @@ import { ApiError, api } from '../../api/client';
import { type OptionType, TreeFilterDropdown } from '../../components/TreeFilterDropdown/TreeFilterDropdown'; import { type OptionType, TreeFilterDropdown } from '../../components/TreeFilterDropdown/TreeFilterDropdown';
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 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 { interface ConsolidationChessTableFilterI {
companies: string[]; companies: string[];
@ -124,6 +115,16 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
} }
} }
const flattenTree = (nodes: OptionType[]): { label: string; value: string }[] => {
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() { function dropFiltersHandler() {
onCompaniesChange([]); onCompaniesChange([]);
onDebitChange([]); onDebitChange([]);
@ -187,7 +188,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
onOpenChange={setDebitOpen} onOpenChange={setDebitOpen}
showSearch={false} showSearch={false}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
options={debitTreeData.map((node) => ({ label: node.code, value: node.key }))} options={flattenTree(debitTreeData)}
popupRender={() => ( popupRender={() => (
<TreeFilterDropdown <TreeFilterDropdown
title='Дебетовые счета' title='Дебетовые счета'
@ -216,7 +217,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
onOpenChange={setCreditOpen} onOpenChange={setCreditOpen}
showSearch={false} showSearch={false}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
options={creditTreeData.map((node) => ({ label: node.code, value: node.key }))} options={flattenTree(creditTreeData)}
popupRender={() => ( popupRender={() => (
<TreeFilterDropdown <TreeFilterDropdown
title='Кредитовые счета' title='Кредитовые счета'