fixed bugs
This commit is contained in:
parent
91fdc6c0f2
commit
cbeb9daa38
@ -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() {
|
||||
<Route path='/groups'>
|
||||
<Route index element={<GroupsLayout />} />
|
||||
<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>
|
||||
<Route path='/catalogue/groups' element={<CatalogueGroup />} />
|
||||
|
||||
@ -80,6 +80,9 @@ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
|
||||
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<TreeFilterDropdownProps> = ({
|
||||
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[])}
|
||||
|
||||
@ -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<VersionCard | null>(null);
|
||||
const { version_id, tab } = useParams<{ tab?: string; version_id: string }>();
|
||||
|
||||
const navigation = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const TAB_COMPONENTS: Record<string, ReactNode> = {
|
||||
osv: <OsvContent card={card} />,
|
||||
|
||||
chess: <ChessContent />,
|
||||
chess: <ChessContent card={card} />,
|
||||
};
|
||||
|
||||
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 (
|
||||
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', height: '100vh', gap: 12 }}>
|
||||
<PageTitle
|
||||
@ -68,7 +80,7 @@ function ReportContent() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ConsolidatioReportPage() {
|
||||
export function ConsolidationReportPage() {
|
||||
const { version_id = '' } = useParams<{ version_id: string }>();
|
||||
// key: при смене версии всё состояние (карточка, фильтры, страница, данные) создаётся заново
|
||||
return <ReportContent key={version_id} />;
|
||||
@ -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';
|
||||
|
||||
@ -18,3 +18,6 @@ export const exportChessTableManual = (versionId: string, data: ChessManualReque
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
export const fetchChessConsolidationsResult = (id: string) =>
|
||||
api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/result-table`);
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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<string[]>([]);
|
||||
const [credit, setCredit] = useState<string[]>([]);
|
||||
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
backgroundColor: isFullscreen ? '#fff' : 'transparent',
|
||||
padding: isFullscreen ? 24 : 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: isFullscreen ? '100vh' : 'auto',
|
||||
overflow: isFullscreen ? 'hidden' : 'visible',
|
||||
}}>
|
||||
<ConsolidationChessTable
|
||||
data={filteredData}
|
||||
loading={loading}
|
||||
decimals={decimals}
|
||||
units={units}
|
||||
isFullscreen={isFullscreen}
|
||||
toolbar={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
flexWrap: 'wrap',
|
||||
gap: 16,
|
||||
}}>
|
||||
<div style={{ flex: '1 1 auto' }}>
|
||||
<ConsolidationChessTableFilter
|
||||
companies={companies}
|
||||
onCompaniesChange={setCompanies}
|
||||
decimals={decimals}
|
||||
onDecimalsChange={setDecimals}
|
||||
units={units}
|
||||
onUnitsChange={setUnits}
|
||||
debit={debit}
|
||||
onDebitChange={setDebit}
|
||||
credit={credit}
|
||||
onCreditChange={setCredit}
|
||||
allCompanies={data?.companies || []}
|
||||
/>
|
||||
<ConfigProvider getPopupContainer={() => containerRef.current || document.body}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
backgroundColor: isFullscreen ? '#fff' : 'transparent',
|
||||
padding: isFullscreen ? 24 : 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: isFullscreen ? '100vh' : 'auto',
|
||||
overflow: isFullscreen ? 'hidden' : 'visible',
|
||||
}}>
|
||||
<ConsolidationChessTable
|
||||
data={filteredData}
|
||||
loading={loading}
|
||||
decimals={decimals}
|
||||
units={units}
|
||||
isFullscreen={isFullscreen}
|
||||
toolbar={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
flexWrap: 'wrap',
|
||||
gap: 16,
|
||||
}}>
|
||||
<div style={{ flex: '1 1 auto' }}>
|
||||
<ConsolidationChessTableFilter
|
||||
companies={companies}
|
||||
onCompaniesChange={setCompanies}
|
||||
decimals={decimals}
|
||||
onDecimalsChange={setDecimals}
|
||||
units={units}
|
||||
onUnitsChange={setUnits}
|
||||
debit={debit}
|
||||
onDebitChange={setDebit}
|
||||
credit={credit}
|
||||
onCreditChange={setCredit}
|
||||
allCompanies={data?.companies || []}
|
||||
/>
|
||||
</div>
|
||||
<Button loading={expoerLoading} onClick={() => download()} icon={<DownloadOutlined />}>
|
||||
Экспорт
|
||||
</Button>
|
||||
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
|
||||
{isFullscreen ? 'Свернуть' : 'На весь экран'}
|
||||
</Button>
|
||||
</div>
|
||||
<Button loading={expoerLoading} onClick={() => download()} icon={<DownloadOutlined />}>
|
||||
Экспорт
|
||||
</Button>
|
||||
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
|
||||
{isFullscreen ? 'Свернуть' : 'На весь экран'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -36,20 +36,53 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
||||
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<ConsolidationChessTableI> = ({
|
||||
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<ConsolidationChessTableI> = ({
|
||||
key: 'name',
|
||||
fixed: 'left',
|
||||
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) => ({
|
||||
@ -92,17 +119,12 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
||||
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 <Text strong>{formatValue(totalVal)}</Text>;
|
||||
}
|
||||
const cell = matrixData[row.id]?.[company.id];
|
||||
if (!cell) return <span style={{ color: '#bfbfbf' }}>—</span>;
|
||||
return (
|
||||
|
||||
@ -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<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() {
|
||||
onCompaniesChange([]);
|
||||
onDebitChange([]);
|
||||
@ -187,7 +188,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
|
||||
onOpenChange={setDebitOpen}
|
||||
showSearch={false}
|
||||
popupMatchSelectWidth={false}
|
||||
options={debitTreeData.map((node) => ({ label: node.code, value: node.key }))}
|
||||
options={flattenTree(debitTreeData)}
|
||||
popupRender={() => (
|
||||
<TreeFilterDropdown
|
||||
title='Дебетовые счета'
|
||||
@ -216,7 +217,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
|
||||
onOpenChange={setCreditOpen}
|
||||
showSearch={false}
|
||||
popupMatchSelectWidth={false}
|
||||
options={creditTreeData.map((node) => ({ label: node.code, value: node.key }))}
|
||||
options={flattenTree(creditTreeData)}
|
||||
popupRender={() => (
|
||||
<TreeFilterDropdown
|
||||
title='Кредитовые счета'
|
||||
Loading…
x
Reference in New Issue
Block a user