добавленна результирующая таблицв ВГО
This commit is contained in:
parent
cbeb9daa38
commit
d1c0f93360
@ -60,6 +60,7 @@ function ReportContent() {
|
|||||||
}, [tab, navigation, pathname]);
|
}, [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 }}>
|
||||||
|
<div style={{ flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
backable
|
backable
|
||||||
location={['Отчётность', groupName, period]}
|
location={['Отчётность', groupName, period]}
|
||||||
@ -75,6 +76,7 @@ function ReportContent() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<OsvTabs />
|
<OsvTabs />
|
||||||
|
</div>
|
||||||
{ActiveTab}
|
{ActiveTab}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { api, apiFetchBlob } from '../../api/client';
|
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) =>
|
export const fetchChessConsolidations = (id: string) =>
|
||||||
api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/all`);
|
api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/all`);
|
||||||
@ -20,4 +20,4 @@ export const exportChessTableManual = (versionId: string, data: ChessManualReque
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const fetchChessConsolidationsResult = (id: string) =>
|
export const fetchChessConsolidationsResult = (id: string) =>
|
||||||
api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/result-table`);
|
api.get<IntercompanyMatrixResultResponse>(`/consolidations/versions/${id}/intercompany-matrix/result-table`);
|
||||||
|
|||||||
44
web/src/utils/chessTableCellFormatValue.ts
Normal file
44
web/src/utils/chessTableCellFormatValue.ts
Normal file
@ -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);
|
||||||
|
};
|
||||||
@ -8,14 +8,16 @@ import {
|
|||||||
exportChessTableManual,
|
exportChessTableManual,
|
||||||
fetchChessConsolidations,
|
fetchChessConsolidations,
|
||||||
fetchChessConsolidationsManual,
|
fetchChessConsolidationsManual,
|
||||||
|
fetchChessConsolidationsResult,
|
||||||
} from '../../services/consodilation/intercompanyMatrix';
|
} 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 { exportFilename, saveBlob } from '../../utils/download';
|
||||||
import { useNotificationApi } from '../../utils/useNotification';
|
import { useNotificationApi } from '../../utils/useNotification';
|
||||||
|
import { ConsolidationChessFormatTableFilter } from '../ConsolidationChessFormatTableFilter/ConsolidationChessFormatTableFilter';
|
||||||
|
import { ConsolidationChessResTable } from '../ConsolidationChessResTable/ConsolidationChessResTable';
|
||||||
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable';
|
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable';
|
||||||
import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter';
|
import { ConsolidationChessTableFilter } from '../ConsolidationChessTableFilter/ConsolidationChessTableFilter';
|
||||||
import { VersionCard } from '../../types/consolidations/version_members';
|
|
||||||
|
|
||||||
|
|
||||||
export const ChessContent = ({ card }: { card: VersionCard | null }) => {
|
export const ChessContent = ({ card }: { card: VersionCard | null }) => {
|
||||||
const { version_id } = useParams<{ version_id: string }>();
|
const { version_id } = useParams<{ version_id: string }>();
|
||||||
@ -30,25 +32,47 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
|
|||||||
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 [chessResData, setChessResData] = useState<IntercompanyMatrixResultResponse | null>(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [chessResUnits, setChessResUnits] = useState('thousand');
|
||||||
|
const [chessResDecimals, setChessResDecimals] = useState(2);
|
||||||
|
const [chessResLoading, setChessResLoading] = useState(false);
|
||||||
|
|
||||||
const toggleFullscreen = async () => {
|
const chessTableRef = useRef<HTMLDivElement>(null);
|
||||||
if (!containerRef.current) return;
|
const resTableRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isChessFullscreen, setIsChessFullscreen] = useState(false);
|
||||||
|
const [isResFullscreen, setIsResFullscreen] = useState(false);
|
||||||
|
|
||||||
|
const toggleFullscreen = async (ref: React.RefObject<HTMLDivElement>, setFullscreen: (val: boolean) => void) => {
|
||||||
|
if (!ref.current) return;
|
||||||
try {
|
try {
|
||||||
if (!document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
await containerRef.current.requestFullscreen();
|
await ref.current.requestFullscreen();
|
||||||
|
setFullscreen(true);
|
||||||
} else {
|
} else {
|
||||||
await document.exitFullscreen();
|
await document.exitFullscreen();
|
||||||
|
setFullscreen(false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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) => {
|
const loadData = async (vId: string, dIds: string[], cIds: string[], isActual: () => boolean) => {
|
||||||
if ((dIds.length > 0 && cIds.length === 0) || (dIds.length === 0 && cIds.length > 0)) {
|
if ((dIds.length > 0 && cIds.length === 0) || (dIds.length === 0 && cIds.length > 0)) {
|
||||||
notification.info({description:'Для сравнений по Дт и Кт нужно заполнить оба фильтра'})
|
notification.info({ description: 'Для сравнений по Дт и Кт нужно заполнить оба фильтра' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
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 () => {
|
const download = async () => {
|
||||||
if (!version_id) {
|
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 ?? ''));
|
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 ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? ''));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ApiError) {
|
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(() => {
|
useEffect(() => {
|
||||||
if (!version_id) return;
|
if (!version_id) return;
|
||||||
|
|
||||||
@ -109,43 +156,38 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
|
|||||||
}, [version_id, debit, credit]);
|
}, [version_id, debit, credit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFullscreenChange = () => {
|
if (!version_id) return;
|
||||||
setIsFullscreen(!!document.fullscreenElement);
|
loadResData(version_id);
|
||||||
};
|
}, [version_id]);
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider getPopupContainer={() => containerRef.current || document.body}>
|
<ConfigProvider getPopupContainer={() => (document.fullscreenElement as HTMLElement) || document.body}>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isFullscreen ? '#fff' : 'transparent',
|
|
||||||
padding: isFullscreen ? 24 : 0,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: isFullscreen ? '100vh' : 'auto',
|
overflowY: 'auto',
|
||||||
overflow: isFullscreen ? 'hidden' : 'visible',
|
overflowX: 'hidden',
|
||||||
|
gap: 24,
|
||||||
|
paddingBottom: 32,
|
||||||
|
width: '100%',
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
ref={chessTableRef}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isChessFullscreen ? '#fff' : 'transparent',
|
||||||
|
padding: isChessFullscreen ? 24 : 0,
|
||||||
|
height: isChessFullscreen ? '100vh' : 'auto',
|
||||||
|
overflow: isChessFullscreen ? 'hidden' : 'visible',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
}}>
|
}}>
|
||||||
<ConsolidationChessTable
|
<ConsolidationChessTable
|
||||||
data={filteredData}
|
data={filteredData}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
decimals={decimals}
|
decimals={decimals}
|
||||||
units={units}
|
units={units}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isChessFullscreen}
|
||||||
toolbar={
|
toolbar={
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -156,31 +198,91 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
|
|||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ flex: '1 1 auto' }}>
|
<div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}>
|
||||||
<ConsolidationChessTableFilter
|
<ConsolidationChessTableFilter
|
||||||
companies={companies}
|
companies={companies}
|
||||||
onCompaniesChange={setCompanies}
|
onCompaniesChange={setCompanies}
|
||||||
decimals={decimals}
|
|
||||||
onDecimalsChange={setDecimals}
|
|
||||||
units={units}
|
|
||||||
onUnitsChange={setUnits}
|
|
||||||
debit={debit}
|
debit={debit}
|
||||||
onDebitChange={setDebit}
|
onDebitChange={setDebit}
|
||||||
credit={credit}
|
credit={credit}
|
||||||
onCreditChange={setCredit}
|
onCreditChange={setCredit}
|
||||||
allCompanies={data?.companies || []}
|
allCompanies={data?.companies || []}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 1,
|
||||||
|
height: 50,
|
||||||
|
backgroundColor: '#e5e7eb',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ConsolidationChessFormatTableFilter
|
||||||
|
decimals={decimals}
|
||||||
|
units={units}
|
||||||
|
onDecimalsChange={setDecimals}
|
||||||
|
onUnitsChange={setUnits}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||||
<Button loading={expoerLoading} onClick={() => download()} icon={<DownloadOutlined />}>
|
<Button loading={expoerLoading} onClick={() => download()} icon={<DownloadOutlined />}>
|
||||||
Экспорт
|
Экспорт
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={toggleFullscreen} icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
|
<Button
|
||||||
{isFullscreen ? 'Свернуть' : 'На весь экран'}
|
onClick={() => toggleFullscreen(chessTableRef, setIsChessFullscreen)}
|
||||||
|
icon={isChessFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
|
||||||
|
{isChessFullscreen ? 'Свернуть' : 'На весь экран'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={resTableRef}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isResFullscreen ? '#fff' : 'transparent',
|
||||||
|
padding: isResFullscreen ? 24 : 0,
|
||||||
|
height: isResFullscreen ? '100vh' : 'auto',
|
||||||
|
overflow: isResFullscreen ? 'hidden' : 'visible',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<ConsolidationChessResTable
|
||||||
|
data={chessResData}
|
||||||
|
loading={chessResLoading}
|
||||||
|
decimals={chessResDecimals}
|
||||||
|
units={chessResUnits}
|
||||||
|
isFullscreen={isResFullscreen}
|
||||||
|
toolbar={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 16,
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap', gap: 16, flex: '1 1 auto' }}>
|
||||||
|
<ConsolidationChessFormatTableFilter
|
||||||
|
decimals={chessResDecimals}
|
||||||
|
units={chessResUnits}
|
||||||
|
onDecimalsChange={setChessResDecimals}
|
||||||
|
onUnitsChange={setChessResUnits}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleFullscreen(resTableRef, setIsResFullscreen)}
|
||||||
|
icon={isResFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}>
|
||||||
|
{isResFullscreen ? 'Свернуть' : 'На весь экран'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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<ConsolidationChessFormatTableFilterI> = ({
|
||||||
|
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 (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 16 }}>
|
||||||
|
<Space size='middle' wrap>
|
||||||
|
<Space orientation='vertical' size={0}>
|
||||||
|
<span style={{ fontSize: 12, color: '#667085' }}>Единицы</span>
|
||||||
|
<Select style={{ minWidth: 120 }} options={UNIT_OPTIONS} value={units} onChange={onUnitsChange} variant='underlined' />
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Space orientation='vertical' size={0}>
|
||||||
|
<span style={{ fontSize: 12, color: '#667085' }}>Знаков после запятой</span>
|
||||||
|
<Select
|
||||||
|
style={{ minWidth: 150 }}
|
||||||
|
value={decimals}
|
||||||
|
onChange={(v) => onDecimalsChange(v || 0)}
|
||||||
|
options={DECIMALS_OPTIONS}
|
||||||
|
variant='underlined'
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 ;
|
||||||
|
}
|
||||||
@ -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<ConsolidationChessResTableI> = ({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
decimals,
|
||||||
|
units,
|
||||||
|
toolbar,
|
||||||
|
isFullscreen = false,
|
||||||
|
}) => {
|
||||||
|
const matrixData = useMemo(() => {
|
||||||
|
if (!data?.articles) return {};
|
||||||
|
const result: Record<string, Record<string, string>> = {};
|
||||||
|
|
||||||
|
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<MatrixResCell> = useMemo(() => {
|
||||||
|
if (!data?.companies) return [];
|
||||||
|
|
||||||
|
const firstColumn: ColumnsType<MatrixResCell>[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 <span style={{ color: '#bfbfbf' }}>—</span>;
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totalColumn: ColumnsType<MatrixResCell>[number] = {
|
||||||
|
title: 'Итого',
|
||||||
|
dataIndex: 'group_total',
|
||||||
|
key: 'group_total',
|
||||||
|
width: 150,
|
||||||
|
align: 'right',
|
||||||
|
render: (val: string) => <Text strong>{chessTableCellFormatValue(val, units, decimals)}</Text>,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Table<MatrixResCell>
|
||||||
|
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 (
|
||||||
|
<Table.Summary fixed>
|
||||||
|
<Table.Summary.Row className='resRow'>
|
||||||
|
<Table.Summary.Cell className='resRow' index={0} align='left'>
|
||||||
|
<div>
|
||||||
|
<Text strong>Итого</Text>
|
||||||
|
</div>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
|
||||||
|
{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 (
|
||||||
|
<Table.Summary.Cell key={company.id} index={index + 1} align='right'>
|
||||||
|
<div>
|
||||||
|
<Text strong>{chessTableCellFormatValue(String(colTotal), units, decimals)}</Text>
|
||||||
|
</div>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Table.Summary.Cell index={data.companies.length + 1} align='right'>
|
||||||
|
<div style={{ backgroundColor: '#e6f4ea', width: '100%', height: '100%' }}>
|
||||||
|
<Text strong>{chessTableCellFormatValue(summaryTotal, units, decimals)}</Text>
|
||||||
|
</div>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
</Table.Summary>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import type React from 'react';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../types/chess/chess';
|
import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../types/chess/chess';
|
||||||
import './ConsolidatedChessTable.css';
|
import './ConsolidatedChessTable.css';
|
||||||
|
import { chessTableCellFormatValue } from '../../utils/chessTableCellFormatValue';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@ -36,55 +37,6 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
|||||||
return result;
|
return result;
|
||||||
}, [data?.cells]);
|
}, [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 => {
|
const parseAmount = (val: string | null | undefined): number => {
|
||||||
if (!val) return 0;
|
if (!val) return 0;
|
||||||
|
|
||||||
@ -109,7 +61,6 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
width: 200,
|
width: 200,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const companyColumns = data.companies.map((company) => ({
|
const companyColumns = data.companies.map((company) => ({
|
||||||
@ -119,7 +70,6 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
|||||||
width: 150,
|
width: 150,
|
||||||
align: 'right' as const,
|
align: 'right' as const,
|
||||||
onCell: (row: TableRow) => {
|
onCell: (row: TableRow) => {
|
||||||
|
|
||||||
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 {};
|
||||||
@ -129,9 +79,11 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
|||||||
if (!cell) return <span style={{ color: '#bfbfbf' }}>—</span>;
|
if (!cell) return <span style={{ color: '#bfbfbf' }}>—</span>;
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 13 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 13 }}>
|
||||||
<span>{formatValue(cell.amount_a)}</span>
|
<span>{chessTableCellFormatValue(cell.amount_a, units, decimals)}</span>
|
||||||
<span>{formatValue(cell.amount_b)}</span>
|
<span>{chessTableCellFormatValue(cell.amount_b, units, decimals)}</span>
|
||||||
<span style={{ color: cell.exceeds_threshold ? 'red' : 'inherit' }}>{formatValue(cell.variance_amount)}</span>
|
<span style={{ color: cell.exceeds_threshold ? 'red' : 'inherit' }}>
|
||||||
|
{chessTableCellFormatValue(cell.variance_amount, units, decimals)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -165,7 +117,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
|||||||
return totalRow as TableRow;
|
return totalRow as TableRow;
|
||||||
}, [data?.companies, matrixData]);
|
}, [data?.companies, matrixData]);
|
||||||
|
|
||||||
const scrollY = isFullscreen ? 'calc(100vh - 180px)' : 436;
|
const scrollY = isFullscreen ? 'calc(100vh - 150px)' : 436;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table<TableRow>
|
<Table<TableRow>
|
||||||
@ -175,7 +127,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={{ x: 'max-content', y: scrollY }}
|
scroll={{ x: 'max-content', y: scrollY }}
|
||||||
size='middle'
|
size='small'
|
||||||
bordered
|
bordered
|
||||||
title={() => toolbar}
|
title={() => toolbar}
|
||||||
summary={() => {
|
summary={() => {
|
||||||
@ -193,7 +145,7 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
|
|||||||
{data?.companies.map((company, index) => (
|
{data?.companies.map((company, index) => (
|
||||||
<Table.Summary.Cell className='resRow' key={company.id} index={index + 1} align='right'>
|
<Table.Summary.Cell className='resRow' key={company.id} index={index + 1} align='right'>
|
||||||
<div>
|
<div>
|
||||||
<Text strong>{formatValue((totalRowData as any)[company.id])}</Text>
|
<Text strong>{chessTableCellFormatValue((totalRowData as any)[company.id], units, decimals)}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -3,24 +3,18 @@ import type React from 'react';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ApiError, api } from '../../api/client';
|
import { ApiError, api } from '../../api/client';
|
||||||
import { type OptionType, TreeFilterDropdown } from '../../components/TreeFilterDropdown/TreeFilterDropdown';
|
import { type OptionType, TreeFilterDropdown } from '../../components/TreeFilterDropdown/TreeFilterDropdown';
|
||||||
|
import type { Account } from '../../types/api';
|
||||||
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 ConsolidationChessTableFilterI {
|
interface ConsolidationChessTableFilterI {
|
||||||
companies: string[];
|
companies: string[];
|
||||||
onCompaniesChange: (val: string[]) => void;
|
onCompaniesChange: (val: string[]) => void;
|
||||||
decimals: number;
|
|
||||||
onDecimalsChange: (val: number) => void;
|
|
||||||
debit: string[];
|
debit: string[];
|
||||||
onDebitChange: (val: string[]) => void;
|
onDebitChange: (val: string[]) => void;
|
||||||
credit: string[];
|
credit: string[];
|
||||||
onCreditChange: (val: string[]) => void;
|
onCreditChange: (val: string[]) => void;
|
||||||
allCompanies: MatrixCompany[];
|
allCompanies: MatrixCompany[];
|
||||||
units: string;
|
|
||||||
onUnitsChange: (val: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildTree = (items: Account[]): OptionType[] => {
|
const buildTree = (items: Account[]): OptionType[] => {
|
||||||
@ -59,15 +53,11 @@ const buildTree = (items: Account[]): OptionType[] => {
|
|||||||
export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilterI> = ({
|
export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilterI> = ({
|
||||||
companies,
|
companies,
|
||||||
onCompaniesChange,
|
onCompaniesChange,
|
||||||
decimals,
|
|
||||||
onDecimalsChange,
|
|
||||||
debit,
|
debit,
|
||||||
onDebitChange,
|
onDebitChange,
|
||||||
credit,
|
credit,
|
||||||
onCreditChange,
|
onCreditChange,
|
||||||
allCompanies,
|
allCompanies,
|
||||||
units,
|
|
||||||
onUnitsChange,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [companyOpen, setCompanyOpen] = useState(false);
|
const [companyOpen, setCompanyOpen] = useState(false);
|
||||||
const [debitOpen, setDebitOpen] = useState(false);
|
const [debitOpen, setDebitOpen] = useState(false);
|
||||||
@ -82,18 +72,6 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
|
|||||||
const companyOptions = allCompanies.map((c) => ({ label: c.name, value: c.id }));
|
const companyOptions = allCompanies.map((c) => ({ label: c.name, value: c.id }));
|
||||||
const companyTreeData = allCompanies.map((c) => ({ title: c.name, key: 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() {
|
async function getOptionHandler() {
|
||||||
setOptionsLoading(true);
|
setOptionsLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -136,7 +114,6 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 16 }}>
|
|
||||||
<Space size='middle' wrap>
|
<Space size='middle' wrap>
|
||||||
<Space orientation='vertical' size={0}>
|
<Space orientation='vertical' size={0}>
|
||||||
<span style={{ fontSize: 12, color: '#667085' }}>Год</span>
|
<span style={{ fontSize: 12, color: '#667085' }}>Год</span>
|
||||||
@ -231,25 +208,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
|
|||||||
loading={optionsLoading}
|
loading={optionsLoading}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
{companies?.length || debit?.length || credit?.length ? (
|
{companies?.length || debit?.length || credit?.length ? <Button onClick={() => dropFiltersHandler()}>Сбросить фильтры</Button> : null}
|
||||||
<Button onClick={() => dropFiltersHandler()}>Сбросить фильтры</Button>
|
|
||||||
) : null}
|
|
||||||
<Space style={{ borderLeft: '1px solid #4754672e', paddingLeft: '8px' }} orientation='vertical' size={0}>
|
|
||||||
<span style={{ fontSize: 12, color: '#667085' }}>Единицы</span>
|
|
||||||
<Select style={{ minWidth: 120 }} options={UNIT_OPTIONS} value={units} onChange={onUnitsChange} variant='underlined' />
|
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Space orientation='vertical' size={0}>
|
|
||||||
<span style={{ fontSize: 12, color: '#667085' }}>Знаков после запятой</span>
|
|
||||||
<Select
|
|
||||||
style={{ minWidth: 150 }}
|
|
||||||
value={decimals}
|
|
||||||
onChange={(v) => onDecimalsChange(v || 0)}
|
|
||||||
options={DECIMALS_OPTIONS}
|
|
||||||
variant='underlined'
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user