added chess table

This commit is contained in:
Grog 2026-10-21 10:51:13 +03:00
parent 7891430fce
commit 9bd8a2a015
11 changed files with 786 additions and 97 deletions

View File

@ -0,0 +1,6 @@
.ant-select-selection-item{
background-color: #ecfdf3 !important;
color: #1f5f35 !important;
box-shadow: inset 0 0 0 1px #2ea64b38 !important;
border-radius: 999px !important;
}

View File

@ -127,10 +127,16 @@ export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise
}
/** Бинарный ответ (скан, xlsx) с токеном и тем же разбором ошибок; имя — из Content-Disposition, если доступно. */
export async function apiFetchBlob(path: string): Promise<{ blob: Blob; filename: string | null }> {
const response = await send(path, {});
export async function apiFetchBlob(
path: string,
init: RequestInit = {} // Добавили init
): Promise<{ blob: Blob; filename: string | null }> {
const response = await send(path, init);
const blob = await response.blob();
return { blob, filename: filenameFromContentDisposition(response.headers.get('Content-Disposition')) };
return {
blob,
filename: filenameFromContentDisposition(response.headers.get('Content-Disposition'))
};
}
function withJsonBody(method: string, body: unknown): RequestInit {

View File

@ -0,0 +1,3 @@
.ant-tree-switcher-noop{
display: none !important;
}

View File

@ -0,0 +1,149 @@
import { useState, useMemo, useEffect } from 'react';
import { Input, Button, Tree, Space } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import './TreeFilterDropdown.css';
export type OptionType = {
title: string;
key: string;
code?:string;
children?: OptionType[];
};
interface TreeFilterDropdownProps {
title: string;
placeholder?: string;
treeData: OptionType[];
selectedKeys: string[];
onChange: (keys: string[]) => void;
onClose: () => void;
}
export const TreeFilterDropdown: React.FC<TreeFilterDropdownProps> = ({
title,
placeholder = 'Введите значение',
treeData,
selectedKeys,
onChange,
onClose,
}) => {
const [query, setQuery] = useState('');
const [checkedKeys, setCheckedKeys] = useState<string[]>(selectedKeys);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
useEffect(() => {
setCheckedKeys(selectedKeys);
}, [selectedKeys]);
const filteredTreeData = useMemo(() => {
if (!query) return treeData;
const filterNode = (node: OptionType): OptionType | null => {
const titleStr = String(node.title).toLowerCase();
const isMatch = titleStr.includes(query.toLowerCase());
let filteredChildren: OptionType[] = [];
if (node.children) {
filteredChildren = node.children.map(filterNode).filter((child): child is OptionType => child !== null);
}
if (isMatch || filteredChildren.length > 0) {
return { ...node, children: filteredChildren.length > 0 ? filteredChildren : undefined };
}
return null;
};
return treeData.map(filterNode).filter((node): node is OptionType => node !== null);
}, [treeData, query]);
useEffect(() => {
if (query) {
const getAllKeys = (nodes: OptionType[]): string[] => {
let keys: string[] = [];
nodes.forEach((node) => {
keys.push(node.key);
if (node.children) keys = [...keys, ...getAllKeys(node.children)];
});
return keys;
};
setExpandedKeys(getAllKeys(filteredTreeData));
}
}, [query, filteredTreeData]);
const getAllNodeKeys = (nodes: OptionType[]): string[] => {
let keys: string[] = [];
nodes.forEach((node) => {
keys.push(node.key);
if (node.children) keys = [...keys, ...getAllNodeKeys(node.children)];
});
return keys;
};
const handleSelectAll = () => {
setCheckedKeys(getAllNodeKeys(treeData));
};
const handleClear = () => {
setCheckedKeys([]);
};
const handleCancel = () => {
setCheckedKeys(selectedKeys);
onClose();
};
return (
<div style={{ padding: 12, width: 380}}>
<div style={{ fontWeight: 600, marginBottom: 12, fontSize: 16 }}>{title}</div>
<div style={{ fontSize: 12, marginBottom: 4 }}>Поиск</div>
<Input
prefix={<SearchOutlined />}
placeholder={placeholder}
value={query}
onChange={(e) => setQuery(e.target.value)}
style={{ marginBottom: 12 }}
/>
<Space style={{ marginBottom: 12, justifyContent: 'space-between', width: '100%' }}>
<Button onClick={handleSelectAll}>
Выбрать все
</Button>
<Button onClick={handleClear}>
Очистить
</Button>
</Space>
<div style={{ maxHeight: 280, overflowY: 'auto', border: '1px solid #f0f0f0', borderRadius: 8, padding: 8 }}>
<Tree
checkable
selectable={false}
treeData={filteredTreeData}
checkedKeys={checkedKeys}
onCheck={(keys) => {
const checked = Array.isArray(keys) ? keys : keys.checked;
setCheckedKeys(checked as string[]);
}}
expandedKeys={expandedKeys}
onExpand={(keys) => setExpandedKeys(keys as string[])}
/>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 10 }}>
<Button onClick={handleCancel}>
Отмена
</Button>
<Button
type='primary'
style={{ background: '#2a8c3a' }}
onClick={() => {
onChange(checkedKeys);
onClose();
}}>
Применить
</Button>
</div>
</div>
);
};

View File

@ -1,4 +1,20 @@
import { api } from "../../api/client";
import { IntercompanyMatrixResponse } from "../../types/chess/chess";
import { api, apiFetchBlob } from '../../api/client';
import { ChessManualRequest, IntercompanyMatrixResponse } from '../../types/chess/chess';
export const fetchChessConsolidations = (id: string) => api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/all`);
export const fetchChessConsolidations = (id: string) =>
api.get<IntercompanyMatrixResponse>(`/consolidations/versions/${id}/intercompany-matrix/all`);
export const fetchChessConsolidationsManual = (versionId: string, data: ChessManualRequest): Promise<IntercompanyMatrixResponse> => {
return api.post<IntercompanyMatrixResponse>(`/consolidations/versions/${versionId}/intercompany-matrix/manual`, data);
};
export const exportChessTableAll = (versionId: string) =>
apiFetchBlob(`/consolidations/versions/${versionId}/intercompany-matrix/all/export`);
export const exportChessTableManual = (versionId: string, data: ChessManualRequest) =>
apiFetchBlob(`/consolidations/versions/${versionId}/intercompany-matrix/manual/export`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

View File

@ -9,9 +9,18 @@ export interface MatrixCell {
amount_a: string;
amount_b: string;
variance_amount: string;
exceeds_threshold:boolean
one_sided:boolean
variance_percent:string| null
skipped_osv_entry_count:number
}
export interface IntercompanyMatrixResponse {
companies: MatrixCompany[];
cells: MatrixCell[];
}
}
export type ChessManualRequest = {
debit_account_ids: string[];
credit_account_ids: string[];
};

View File

@ -1,6 +0,0 @@
.empty-cell {
background-color: #f2f4f7;
}
.ant-table-tbody > tr:hover > td.empty-cell {
background-color: #f2f4f7 !important;
}

View File

@ -1,15 +1,22 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Table, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useEffect, useMemo, useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { Button } from 'antd';
import { FullscreenOutlined, FullscreenExitOutlined, DownloadOutlined } from '@ant-design/icons';
import { ApiError } from '../../api/client';
import { fetchChessConsolidations } from '../../services/consodilation/chess';
import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../types/chess/chess';
import {
exportChessTableAll,
exportChessTableManual,
fetchChessConsolidations,
fetchChessConsolidationsManual,
} from '../../services/consodilation/chess';
import type { IntercompanyMatrixResponse } from '../../types/chess/chess';
import { useNotificationApi } from '../../utils/useNotification';
import { ConsolidationChessTable } from '../ConsolidationChessTable/ConsolidationChessTable';
import { ConsolidationChessTableFilter } from '../ConsolidationChessTable/ConsolidationChessTableFilter';
import { saveBlob } from '../../utils/download';
import './ConsolidatedChessContent.css';
const { Text } = Typography;
export const ChessContent = () => {
const { version_id } = useParams<{ version_id: string }>();
@ -17,103 +24,147 @@ export const ChessContent = () => {
const [data, setData] = useState<IntercompanyMatrixResponse | null>(null);
const [loading, setLoading] = useState(false);
const [expoerLoading, setExportLoading] = useState(false);
const [companies, setCompanies] = useState<string[]>([]);
const [decimals, setDecimals] = useState(2);
const [units, setUnits] = useState('thousand');
const [debit, setDebit] = useState<string[]>([]);
const [credit, setCredit] = useState<string[]>([]);
const loadData = async (vId: string) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const toggleFullscreen = async () => {
if (!containerRef.current) return;
try {
if (!document.fullscreenElement) {
await containerRef.current.requestFullscreen();
} else {
await document.exitFullscreen();
}
} catch (e) {
console.log(e);
}
};
const loadData = async (vId: string, dIds: string[], cIds: string[]) => {
setLoading(true);
try {
const res = await fetchChessConsolidations(vId);
let res;
if (dIds?.length && cIds?.length) {
res = await fetchChessConsolidationsManual(vId, { debit_account_ids: dIds, credit_account_ids: cIds });
} else {
res = await fetchChessConsolidations(vId);
}
setData(res);
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
}
}
} finally {
setLoading(false);
}
};
const download = async () => {
if (!version_id) {
return;
}
setExportLoading(true);
try {
if (debit?.length && credit?.length) {
const { blob, filename } = await exportChessTableManual(version_id, { debit_account_ids: debit, credit_account_ids: credit });
saveBlob(blob, filename ?? 'Шахматка');
} else {
const { blob, filename } = await exportChessTableAll(version_id);
saveBlob(blob, filename ?? 'Шахматка');
}
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
setExportLoading(false);
}
};
useEffect(() => {
if (!version_id) return;
loadData(version_id);
}, [version_id]);
loadData(version_id, debit, credit);
}, [version_id, debit, credit]);
const matrixData = useMemo(() => {
if (!data?.cells) return {};
const result: Record<string, Record<string, MatrixCell>> = {};
data.cells.forEach((cell) => {
if (!result[cell.company_a_id]) result[cell.company_a_id] = {};
result[cell.company_a_id][cell.company_b_id] = cell;
});
return result;
}, [data?.cells]);
const columns: ColumnsType<MatrixCompany> = useMemo(() => {
if (!data?.companies) return [];
const firstColumn: ColumnsType<MatrixCompany>[number] = {
title: 'ДБ / КТ',
dataIndex: 'name',
key: 'name',
fixed: 'left',
width: 200,
render: (text) => <Text strong>{text}</Text>,
onCell: () => ({ style: { background: '#fff', zIndex: 2 } }),
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
// Внутри companyColumns
const companyColumns = data.companies.map((company) => ({
title: company.name,
dataIndex: company.id,
key: company.id,
width: 150,
align: 'right' as const,
const filteredData = useMemo(() => {
if (!data) return null;
if (companies.length === 0) return data;
onCell: (row: MatrixCompany) => {
const cell = matrixData[row.id]?.[company.id];
if (!cell) {
// Возвращаем класс через onCell, это работает надежнее
return { className: 'empty-cell' };
}
return {};
},
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));
render: (_: unknown, row: MatrixCompany) => {
const cell = matrixData[row.id]?.[company.id];
if (!cell) {
return <span style={{ color: '#bfbfbf' }}>—</span>;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 13 }}>
<span>{cell.amount_a}</span>
<span>{cell.amount_b}</span>
<span
style={{
color: cell.variance_amount.startsWith('-') || cell.variance_amount.startsWith('(') ? 'red' : 'inherit',
}}>
{cell.variance_amount}
</span>
</div>
);
},
}));
return [firstColumn, ...companyColumns];
}, [data?.companies, matrixData]);
return { companies: filteredCompanies, cells: filteredCells };
}, [data, companies]);
return (
<Table<MatrixCompany>
dataSource={data?.companies || []}
columns={columns}
rowKey='id'
pagination={false}
loading={loading}
scroll={{ x: 'max-content', y: 'calc(100vh - 280px)' }}
size='small'
bordered
/>
<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>
}
/>
</div>
);
};

View File

@ -0,0 +1,19 @@
.empty-cell {
background-color: #f2f4f7;
}
.ant-table-tbody > tr:hover > td.empty-cell {
background-color: #f2f4f7 !important;
}
.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 ;
}

View File

@ -0,0 +1,183 @@
import React, { useMemo } from 'react';
import { Table, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../types/chess/chess';
import './ConsolidatedChessTable.css';
const { Text } = Typography;
interface ConsolidationChessTableI {
data: IntercompanyMatrixResponse | null;
loading: boolean;
decimals: number;
units: string;
toolbar?: React.ReactNode;
isFullscreen?: boolean;
}
type TableRow = MatrixCompany & { isTotal?: boolean };
export const ConsolidationChessTable: React.FC<ConsolidationChessTableI> = ({
data,
loading,
decimals,
units,
toolbar,
isFullscreen = false,
}) => {
const matrixData = useMemo(() => {
if (!data?.cells) return {};
const result: Record<string, Record<string, MatrixCell>> = {};
data.cells.forEach((cell) => {
if (!result[cell.company_a_id]) result[cell.company_a_id] = {};
result[cell.company_a_id][cell.company_b_id] = cell;
});
return result;
}, [data?.cells]);
const formatValue = (val: string | number) => {
if (val === undefined || val === null || val === '') return '';
let num = typeof val === 'string' ? parseFloat(val) : val;
if (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;
}
return num.toFixed(decimals).replace('.', ',');
};
const parseAmount = (val: string | null | undefined): number => {
if (!val) return 0;
const isNegative: boolean = val.startsWith('(') && val.endsWith(')');
const cleanVal = isNegative ? val.slice(1, -1) : val;
const num = parseFloat(cleanVal);
if (isNaN(num)) {
return 0;
}
return isNegative ? -num : num;
};
const columns: ColumnsType<TableRow> = useMemo(() => {
if (!data?.companies) return [];
const firstColumn: ColumnsType<TableRow>[number] = {
title: 'ДБ / КТ',
dataIndex: 'name',
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) => ({
title: company.name,
dataIndex: company.id,
key: company.id,
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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 13 }}>
<span>{formatValue(cell.amount_a)}</span>
<span>{formatValue(cell.amount_b)}</span>
<span style={{ color: cell.exceeds_threshold ? 'red' : 'inherit' }}>{formatValue(cell.variance_amount)}</span>
</div>
);
},
}));
return [firstColumn, ...companyColumns];
}, [data?.companies, matrixData, decimals, units]);
const totalRowData = useMemo(() => {
if (!data?.companies) return null;
const totalRow: Record<string, any> = {
id: 'total-row',
name: 'Итого',
isTotal: true,
};
data.companies.forEach((colCompany) => {
let colTotal = 0;
data.companies.forEach((rowCompany) => {
const cell = matrixData[rowCompany.id]?.[colCompany.id];
if (cell?.variance_amount) {
colTotal += parseAmount(cell.variance_amount);
}
});
totalRow[colCompany.id] = colTotal;
});
return totalRow as TableRow;
}, [data?.companies, matrixData]);
const scrollY = isFullscreen ? 'calc(100vh - 180px)' : 436;
return (
<Table<TableRow>
dataSource={data?.companies || []}
columns={columns}
rowKey='id'
pagination={false}
loading={loading}
scroll={{ x: 'max-content', y: scrollY }}
size='middle'
bordered
title={() => toolbar}
summary={() => {
if (!totalRowData) return null;
return (
<Table.Summary fixed >
<Table.Summary.Row className='resRow'>
<Table.Summary.Cell className='resRow' index={0} align='left'>
<div style={{ backgroundColor: '#e6f4ea', width: '100%', height: '100%' }}>
<Text strong>Итого</Text>
</div>
</Table.Summary.Cell>
{data?.companies.map((company, index) => (
<Table.Summary.Cell className='resRow' key={company.id} index={index + 1} align='right'>
<div>
<Text strong>{formatValue((totalRowData as any)[company.id])}</Text>
</div>
</Table.Summary.Cell>
))}
</Table.Summary.Row>
</Table.Summary>
);
}}
/>
);
};

View File

@ -0,0 +1,253 @@
import React, { useEffect, useState } from 'react';
import { Space, Select, Button } from 'antd';
import type { MatrixCompany } from '../../types/chess/chess';
import { OptionType, TreeFilterDropdown } from '../../components/TreeFilterDropdown/TreeFilterDropdown';
import { api, ApiError } from '../../api/client';
import { useNotificationApi } from '../../utils/useNotification';
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[];
onCompaniesChange: (val: string[]) => void;
decimals: number;
onDecimalsChange: (val: number) => void;
debit: string[];
onDebitChange: (val: string[]) => void;
credit: string[];
onCreditChange: (val: string[]) => void;
allCompanies: MatrixCompany[];
units: string;
onUnitsChange: (val: string) => void;
}
const buildTree = (items: Account[]): OptionType[] => {
const map: Record<string, any> = {};
const roots: OptionType[] = [];
items.forEach((item) => {
map[item.id] = {
title: `${item.code} - ${item.name}`,
key: item.id,
code: item.code,
children: [],
};
});
items.forEach((item) => {
if (item.parent_id && map[item.parent_id]) {
map[item.parent_id].children.push(map[item.id]);
} else {
roots.push(map[item.id]);
}
});
const cleanUp = (nodes: OptionType[]): OptionType[] => {
return nodes.map((node) => {
if (node.children && node.children.length > 0) {
return { ...node, children: cleanUp(node.children) };
}
return { title: node.title, key: node.key, code: node.code };
});
};
return cleanUp(roots);
};
export const ConsolidationChessTableFilter: React.FC<ConsolidationChessTableFilterI> = ({
companies,
onCompaniesChange,
decimals,
onDecimalsChange,
debit,
onDebitChange,
credit,
onCreditChange,
allCompanies,
units,
onUnitsChange,
}) => {
const [companyOpen, setCompanyOpen] = useState(false);
const [debitOpen, setDebitOpen] = useState(false);
const [creditOpen, setCreditOpen] = useState(false);
const [optionsLoading, setOptionsLoading] = useState(false);
const [debitTreeData, setDebitTreeData] = useState<OptionType[]>([]);
const [creditTreeData, setCreditTreeData] = useState<OptionType[]>([]);
const notification = useNotificationApi();
const companyOptions = allCompanies.map((c) => ({ label: c.name, value: 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() {
setOptionsLoading(true);
try {
const res = await api.get<Account[]>('/directories/accounts');
const debitAccounts = res.filter((acc) => acc.is_debit);
const creditAccounts = res.filter((acc) => acc.is_credit);
setDebitTreeData(buildTree(debitAccounts));
setCreditTreeData(buildTree(creditAccounts));
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
} else {
notification.error({ description: 'Не удалось загрузить счета' });
}
} finally {
setOptionsLoading(false);
}
}
function dropFiltersHandler() {
onCompaniesChange([]);
onDebitChange([]);
onCreditChange([]);
}
useEffect(() => {
getOptionHandler();
}, []);
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 disabled style={{ minWidth: 150 }} value={null} options={[]} variant='underlined' />
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Квартал</span>
<Select disabled style={{ minWidth: 150 }} value={null} options={[]} variant='underlined' />
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Компании</span>
<Select
mode='multiple'
style={{ minWidth: 200 }}
placeholder='Все компании'
options={companyOptions}
value={companies}
onChange={onCompaniesChange}
maxTagCount='responsive'
open={companyOpen}
onOpenChange={setCompanyOpen}
variant='underlined'
showSearch={false}
popupMatchSelectWidth={false}
popupRender={() => (
<TreeFilterDropdown
title='Компании'
placeholder='Введите название компании'
treeData={companyTreeData}
selectedKeys={companies}
onChange={onCompaniesChange}
onClose={() => setCompanyOpen(false)}
/>
)}
/>
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Дт</span>
<Select
mode='multiple'
style={{ minWidth: 150 }}
placeholder='Все счета'
value={debit}
onChange={onDebitChange}
maxTagCount='responsive'
variant='underlined'
open={debitOpen}
onOpenChange={setDebitOpen}
showSearch={false}
popupMatchSelectWidth={false}
options={debitTreeData.map((node) => ({ label: node.code, value: node.key }))}
popupRender={() => (
<TreeFilterDropdown
title='Дебетовые счета'
placeholder='Введите номер или название счета'
treeData={debitTreeData}
selectedKeys={debit}
onChange={onDebitChange}
onClose={() => setDebitOpen(false)}
/>
)}
loading={optionsLoading}
/>
</Space>
<Space orientation='vertical' size={0}>
<span style={{ fontSize: 12, color: '#667085' }}>Кт</span>
<Select
mode='multiple'
style={{ minWidth: 150 }}
placeholder='Все счета'
value={credit}
onChange={onCreditChange}
maxTagCount='responsive'
variant='underlined'
open={creditOpen}
onOpenChange={setCreditOpen}
showSearch={false}
popupMatchSelectWidth={false}
options={creditTreeData.map((node) => ({ label: node.code, value: node.key }))}
popupRender={() => (
<TreeFilterDropdown
title='Кредитовые счета'
placeholder='Введите номер или название счета'
treeData={creditTreeData}
selectedKeys={credit}
onChange={onCreditChange}
onClose={() => setCreditOpen(false)}
/>
)}
loading={optionsLoading}
/>
</Space>
{companies?.length || debit?.length || credit?.length ? (
<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 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>
);
};