исправленны баги пулл реквеста (обновленны стили, добалвенно уведомление о фильтрах ДТ и Кт, добавленна возможность выбирать рубли в фильтре единиц исчисления, )

This commit is contained in:
Grog 2026-10-22 14:28:47 +03:00
parent 473492f6f0
commit ea9cb4a084
10 changed files with 75 additions and 87 deletions

View File

@ -1,4 +1,4 @@
.ant-select-selection-item { .custom-select-selection-item .ant-select-selection-item {
background-color: #ecfdf3 !important; background-color: #ecfdf3 !important;
color: #1f5f35 !important; color: #1f5f35 !important;
box-shadow: inset 0 0 0 1px #2ea64b38 !important; box-shadow: inset 0 0 0 1px #2ea64b38 !important;

View File

@ -1,6 +1,6 @@
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { Button, Input, Space, Tree } from 'antd'; import { Button, Input, Space, Tree } from 'antd';
import { useEffect, useMemo, useState } from 'react'; import { ReactNode, useEffect, useMemo, useState } from 'react';
import './TreeFilterDropdown.css'; import './TreeFilterDropdown.css';
export type OptionType = { export type OptionType = {
@ -17,6 +17,7 @@ interface TreeFilterDropdownProps {
selectedKeys: string[]; selectedKeys: string[];
onChange: (keys: string[]) => void; onChange: (keys: string[]) => void;
onClose: () => void; onClose: () => void;
warningBlock?: ReactNode
} }
export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
@ -26,6 +27,7 @@ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
selectedKeys, selectedKeys,
onChange, onChange,
onClose, onClose,
warningBlock,
}) => { }) => {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
@ -99,7 +101,7 @@ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
return ( return (
<div style={{ padding: 12, width: 380 }}> <div style={{ padding: 12, width: 380 }}>
<div style={{ fontWeight: 600, marginBottom: 12, fontSize: 16 }}>{title}</div> <div style={{ fontWeight: 600, marginBottom: 12, fontSize: 16 }}>{title}</div>
{warningBlock}
<div style={{ fontSize: 12, marginBottom: 4 }}>Поиск</div> <div style={{ fontSize: 12, marginBottom: 4 }}>Поиск</div>
<Input <Input
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
@ -116,32 +118,19 @@ export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
<div style={{ maxHeight: 280, overflowY: 'auto', border: '1px solid #f0f0f0', borderRadius: 8, padding: 8 }}> <div style={{ maxHeight: 280, overflowY: 'auto', border: '1px solid #f0f0f0', borderRadius: 8, padding: 8 }}>
<Tree <Tree
className='filterTree'
checkable checkable
selectable={false} selectable={false}
treeData={filteredTreeData} treeData={filteredTreeData}
checkedKeys={visibleCheckedKeys} checkedKeys={visibleCheckedKeys}
onCheck={(keys, e) => { onCheck={(keys) => {
const newChecked = Array.isArray(keys) ? keys : keys.checked; const newChecked = Array.isArray(keys) ? keys : keys.checked;
const targetKey = e.node.key as string;
const isChecked = newChecked.includes(targetKey);
if (isChecked) { const allVisibleKeys = getAllNodeKeys(filteredTreeData);
setCheckedKeys(Array.from(new Set([...checkedKeys, ...newChecked.map(String)]))); const hiddenKeys = checkedKeys.filter((k) => !allVisibleKeys.includes(k));
} 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))); const finalKeys = Array.from(new Set([...newChecked.map(String), ...hiddenKeys]));
}
setCheckedKeys(finalKeys);
}} }}
expandedKeys={expandedKeys} expandedKeys={expandedKeys}
onExpand={(keys) => setExpandedKeys(keys as string[])} onExpand={(keys) => setExpandedKeys(keys as string[])}

View File

@ -1,4 +1,8 @@
export const chessTableCellFormatValue = (val: string | number | null | undefined, units: string, decimals: number) => { export const chessTableCellFormatValue = (
val: string | number | null | undefined,
units: string,
decimals: number
) => {
if (val === undefined || val === null || val === '') return ''; if (val === undefined || val === null || val === '') return '';
let strVal = String(val); let strVal = String(val);
@ -30,15 +34,22 @@ export const chessTableCellFormatValue = (val: string | number | null | undefine
newFrac = `${intPart.slice(-shift)}${fracPart}`; newFrac = `${intPart.slice(-shift)}${fracPart}`;
} }
strVal = `${isNegative ? '-' : ''}${newInt}.${newFrac.slice(0, 4)}`; strVal = `${isNegative ? '-' : ''}${newInt}.${newFrac}`;
} }
} }
const num = Number(strVal); const num = Number(strVal);
if (Number.isNaN(num)) return strVal; if (Number.isNaN(num)) return strVal;
return new Intl.NumberFormat('ru-RU', { const formatted = new Intl.NumberFormat('ru-RU', {
minimumFractionDigits: decimals, minimumFractionDigits: decimals,
maximumFractionDigits: decimals, maximumFractionDigits: decimals,
}).format(num); }).format(strVal as any);
const zeroString = `0${decimals > 0 ? ',' + '0'.repeat(decimals) : ''}`;
if (formatted === `-${zeroString}`) {
return zeroString;
}
return formatted;
}; };

View File

@ -213,6 +213,7 @@ export const CatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, jurId
label='Альтернативные наименования' label='Альтернативные наименования'
extra='Введите синоним и нажмите Enter. Можно добавить несколько значений.'> extra='Введите синоним и нажмите Enter. Можно добавить несколько значений.'>
<Select <Select
className='custom-select-selection-item'
mode='tags' mode='tags'
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder='Добавить синоним' placeholder='Добавить синоним'

View File

@ -54,9 +54,9 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
setFullscreen(false); setFullscreen(false);
} }
} catch (e) { } catch (e) {
if (e instanceof ApiError) { notification.error({
notification.error({ description: e.message }); description: e instanceof Error ? e.message : 'Не удалось переключить полноэкранный режим',
} });
} }
}; };
@ -119,10 +119,10 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
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 ?? 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 ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? '')); 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) {
@ -139,7 +139,10 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
setExportLoading(true); setExportLoading(true);
try { try {
const { blob, filename } = await exportChessConsolidationsResult(version_id); const { blob, filename } = await exportChessConsolidationsResult(version_id);
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 ?? ''),
);
} catch (e) { } catch (e) {
if (e instanceof ApiError) { if (e instanceof ApiError) {
notification.error({ description: e.message }); notification.error({ description: e.message });
@ -169,6 +172,7 @@ export const ChessContent = ({ card }: { card: VersionCard | null }) => {
return () => { return () => {
isMounted = false; isMounted = false;
setLoading(false);
}; };
}, [version_id, debit, credit]); }, [version_id, debit, credit]);

View File

@ -15,6 +15,7 @@ export const ConsolidationChessFormatTableFilter: React.FC<ConsolidationChessFor
onUnitsChange, onUnitsChange,
}) => { }) => {
const UNIT_OPTIONS = [ const UNIT_OPTIONS = [
{ value: 'rub', label: 'руб.' },
{ value: 'thousand', label: 'тыс. руб.' }, { value: 'thousand', label: 'тыс. руб.' },
{ value: 'million', label: 'млн. руб.' }, { value: 'million', label: 'млн. руб.' },
{ value: 'billion', label: 'млрд руб.' }, { value: 'billion', label: 'млрд руб.' },

View File

@ -5,6 +5,7 @@ import { useMemo } from 'react';
import type { IntercompanyMatrixResultResponse, MatrixResCell } from '../../types/chess/chess'; import type { IntercompanyMatrixResultResponse, MatrixResCell } from '../../types/chess/chess';
import './ConsolidatedChessResTable.css'; import './ConsolidatedChessResTable.css';
import { chessTableCellFormatValue } from '../../utils/chessTableCellFormatValue'; import { chessTableCellFormatValue } from '../../utils/chessTableCellFormatValue';
import { sumAmounts } from '../../utils/money';
const { Text } = Typography; const { Text } = Typography;
@ -48,7 +49,6 @@ export const ConsolidationChessResTable: React.FC<ConsolidationChessResTableI> =
key: 'name', key: 'name',
fixed: 'left', fixed: 'left',
width: 250, width: 250,
}; };
const companyColumns = data.companies.map((company) => ({ const companyColumns = data.companies.map((company) => ({
@ -80,16 +80,6 @@ export const ConsolidationChessResTable: React.FC<ConsolidationChessResTableI> =
return [firstColumn, ...companyColumns, totalColumn]; return [firstColumn, ...companyColumns, totalColumn];
}, [data?.companies, matrixData, decimals, units]); }, [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; const scrollY = isFullscreen ? 'calc(100vh - 180px)' : 436;
return ( return (
@ -117,27 +107,23 @@ export const ConsolidationChessResTable: React.FC<ConsolidationChessResTableI> =
</Table.Summary.Cell> </Table.Summary.Cell>
{data.companies.map((company, index) => { {data.companies.map((company, index) => {
let colTotal = 0; const amountsToSum: string[] = [];
data.articles.forEach((article) => { data.articles.forEach((article) => {
const amount = matrixData[article.code]?.[company.id]; const amount = matrixData[article.code]?.[company.id];
if (amount && amount !== '0') { if (amount) amountsToSum.push(amount);
colTotal += Number(amount);
}
}); });
const colTotal = sumAmounts(amountsToSum) ?? '0';
return ( return (
<Table.Summary.Cell key={company.id} index={index + 1} align='right'> <Table.Summary.Cell key={company.id} index={index + 1} align='right'>
<div> <Text strong>{chessTableCellFormatValue(colTotal, units, decimals)}</Text>
<Text strong>{chessTableCellFormatValue(String(colTotal), units, decimals)}</Text>
</div>
</Table.Summary.Cell> </Table.Summary.Cell>
); );
})} })}
<Table.Summary.Cell index={data.companies.length + 1} align='right' > <Table.Summary.Cell index={data.companies.length + 1} align='right' >
<div style={{ backgroundColor: '#e6f4ea', width: '100%', height: '100%' }}> <Text strong>{chessTableCellFormatValue(sumAmounts(data.articles.map((a) => a.group_total)) ?? '0', units, decimals)}</Text>
<Text strong>{chessTableCellFormatValue(summaryTotal, units, decimals)}</Text>
</div>
</Table.Summary.Cell> </Table.Summary.Cell>
</Table.Summary.Row> </Table.Summary.Row>
</Table.Summary> </Table.Summary>

View File

@ -5,6 +5,7 @@ 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'; import { chessTableCellFormatValue } from '../../utils/chessTableCellFormatValue';
import { sumAmounts } from '../../utils/money';
const { Text } = Typography; const { Text } = Typography;
@ -17,7 +18,12 @@ interface ConsolidationChessTableI {
isFullscreen?: boolean; isFullscreen?: boolean;
} }
type TableRow = MatrixCompany & { isTotal?: boolean };
type TotalRow = {
id: string;
name: string;
[key: string]: string; // Здесь будут храниться суммы по ID компаний
};
export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
data, data,
@ -37,25 +43,10 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
return result; return result;
}, [data?.cells]); }, [data?.cells]);
const parseAmount = (val: string | null | undefined): number => { const columns: ColumnsType<MatrixCompany> = useMemo(() => {
if (!val) return 0;
const isNegative: boolean = val.startsWith('(') && val.endsWith(')');
const cleanVal = isNegative ? val.slice(1, -1) : val;
const num = Number(cleanVal);
if (Number.isNaN(num)) {
return 0;
}
return isNegative ? -num : num;
};
const columns: ColumnsType<TableRow> = useMemo(() => {
if (!data?.companies) return []; if (!data?.companies) return [];
const firstColumn: ColumnsType<TableRow>[number] = { const firstColumn: ColumnsType<MatrixCompany>[number] = {
title: 'ДБ / КТ', title: 'ДБ / КТ',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
@ -69,12 +60,12 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
key: company.id, key: company.id,
width: 150, width: 150,
align: 'right' as const, align: 'right' as const,
onCell: (row: TableRow) => { onCell: (row: MatrixCompany) => {
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: MatrixCompany) => {
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 (
@ -95,32 +86,31 @@ export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
const totalRowData = useMemo(() => { const totalRowData = useMemo(() => {
if (!data?.companies) return null; if (!data?.companies) return null;
const totalRow: Record<string, any> = { const totalRow: TotalRow = {
id: 'total-row', id: 'total-row',
name: 'Итого', name: 'Итого',
isTotal: true,
}; };
data.companies.forEach((colCompany) => { data.companies.forEach((colCompany) => {
let colTotal = 0; const amountsToSum: string[] = [];
data.companies.forEach((rowCompany) => { data.companies.forEach((rowCompany) => {
const cell = matrixData[rowCompany.id]?.[colCompany.id]; const cell = matrixData[rowCompany.id]?.[colCompany.id];
if (cell?.variance_amount) { if (cell?.amount_a) {
colTotal += parseAmount(cell.variance_amount); amountsToSum.push(cell.amount_a);
} }
}); });
totalRow[colCompany.id] = colTotal; totalRow[colCompany.id] = sumAmounts(amountsToSum) ?? '0';
}); });
return totalRow as TableRow; return totalRow as MatrixCompany;
}, [data?.companies, matrixData]); }, [data?.companies, matrixData]);
const scrollY = isFullscreen ? 'calc(100vh - 150px)' : 436; const scrollY = isFullscreen ? 'calc(100vh - 150px)' : 436;
return ( return (
<Table<TableRow> <Table<MatrixCompany>
className='chessTable' className='chessTable'
dataSource={data?.companies || []} dataSource={data?.companies || []}
columns={columns} columns={columns}
@ -146,7 +136,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>{chessTableCellFormatValue((totalRowData as any)[company.id], units, decimals)}</Text> <Text strong>{chessTableCellFormatValue((totalRowData as TotalRow)[company.id], units, decimals)}</Text>
</div> </div>
</Table.Summary.Cell> </Table.Summary.Cell>
))} ))}

View File

@ -18,7 +18,7 @@ interface ConsolidationChessTableFilterI {
} }
const buildTree = (items: Account[]): OptionType[] => { const buildTree = (items: Account[]): OptionType[] => {
const map: Record<string, any> = {}; const map: Record<string, OptionType> = {};
const roots: OptionType[] = []; const roots: OptionType[] = [];
items.forEach((item) => { items.forEach((item) => {
@ -32,7 +32,7 @@ const buildTree = (items: Account[]): OptionType[] => {
items.forEach((item) => { items.forEach((item) => {
if (item.parent_id && map[item.parent_id]) { if (item.parent_id && map[item.parent_id]) {
map[item.parent_id].children.push(map[item.id]); map[item.parent_id].children?.push(map[item.id]);
} else { } else {
roots.push(map[item.id]); roots.push(map[item.id]);
} }
@ -113,6 +113,8 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
getOptionHandler(); getOptionHandler();
}, []); }, []);
const warningBlock = <span style={{ color: '#d46b08', fontSize: 13 }}>Выберите счета по Дт и Кт для фильтрации</span>;
return ( return (
<Space size='middle' wrap> <Space size='middle' wrap>
<Space orientation='vertical' size={0}> <Space orientation='vertical' size={0}>
@ -138,6 +140,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
variant='underlined' variant='underlined'
showSearch={false} showSearch={false}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
className='custom-select-selection-item '
popupRender={() => ( popupRender={() => (
<TreeFilterDropdown <TreeFilterDropdown
title='Компании' title='Компании'
@ -166,6 +169,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
showSearch={false} showSearch={false}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
options={flattenTree(debitTreeData)} options={flattenTree(debitTreeData)}
className='custom-select-selection-item '
popupRender={() => ( popupRender={() => (
<TreeFilterDropdown <TreeFilterDropdown
title='Дебетовые счета' title='Дебетовые счета'
@ -174,6 +178,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
selectedKeys={debit} selectedKeys={debit}
onChange={onDebitChange} onChange={onDebitChange}
onClose={() => setDebitOpen(false)} onClose={() => setDebitOpen(false)}
warningBlock={warningBlock}
/> />
)} )}
loading={optionsLoading} loading={optionsLoading}
@ -203,6 +208,7 @@ export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilt
selectedKeys={credit} selectedKeys={credit}
onChange={onCreditChange} onChange={onCreditChange}
onClose={() => setCreditOpen(false)} onClose={() => setCreditOpen(false)}
warningBlock={warningBlock}
/> />
)} )}
loading={optionsLoading} loading={optionsLoading}