fix: выравнивание ИНН при создании новой консолидации, + и убраны звёзды обязательности

This commit is contained in:
maxim 2026-10-20 13:14:23 +03:00
parent a7600444e6
commit 2e75de33cc
2 changed files with 127 additions and 86 deletions

View File

@ -0,0 +1,120 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { LegalEntityBrief } from '../../types/api';
import { Checkbox, Empty, Input, Spin } from 'antd';
type RowProps = {
entity: LegalEntityBrief;
checked: boolean;
onToggle: (id: string) => void;
};
const CompanyOptionRow = memo(({ entity, checked, onToggle }: RowProps) => (
<div
role='checkbox'
aria-checked={checked}
tabIndex={0}
onClick={() => onToggle(entity.id)}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onToggle(entity.id);
}
}}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
minWidth: 0,
gap: 12,
padding: '10px 8px',
borderRadius: 4,
borderBottom: '1px solid rgba(0, 0, 0, 0.05)',
cursor: 'pointer',
boxSizing: 'border-box',
}}>
<span
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
flex: 1,
minWidth: 0,
overflow: 'hidden',
}}>
<Checkbox checked={checked} tabIndex={-1} style={{ pointerEvents: 'none' }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{entity.name}</span>
</span>
<span style={{ color: '#8c8c8c', flexShrink: 0 }}>{entity.inn}</span>
</div>
));
type CompanyMultiSelectProps = {
companies: LegalEntityBrief[];
loading?: boolean;
// injected by Form.Item:
value?: string[];
onChange?: (value: string[]) => void;
};
export const CompanyMultiSelect: React.FC<CompanyMultiSelectProps> = ({ companies, loading, value = [], onChange }) => {
const [search, setSearch] = useState('');
const toggle = useCallback(
(id: string) => {
const next = value.includes(id) ? value.filter((v) => v !== id) : [...value, id];
onChange?.(next);
},
[value, onChange],
);
const filtered = useMemo(() => {
if (!search) return companies;
const q = search.toLowerCase();
return companies.filter((s) => s.name.toLowerCase().includes(q));
}, [companies, search]);
return (
<div style={{ border: '1px solid #d9d9d9', borderRadius: 8, padding: 12 }} className='multiselect'>
<Input.Search
placeholder='Поиск по ЮЛ'
allowClear
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ marginBottom: 12 }}
/>
{loading ? (
<div style={{ textAlign: 'center', padding: 24 }}>
<Spin />
</div>
) : null}
{!loading && filtered.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={companies.length === 0 ? 'Нет компаний' : 'Нет совпадений'} />
) : (
<div
style={{
maxHeight: 200,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 4,
paddingRight: 16,
overflowX: 'hidden',
}}>
{filtered.map((s) => (
<CompanyOptionRow key={s.id} entity={s} checked={value.includes(s.id)} onToggle={toggle} />
))}
</div>
)}
{!loading && companies.length > 0 && (
<div style={{ marginTop: 8, display: 'flex', gap: 12 }}>
<span style={{ marginLeft: 'auto', color: '#8c8c8c' }}>
Выбрано: {value.length} / {companies.length}
</span>
</div>
)}
</div>
);
};

View File

@ -1,4 +1,4 @@
import { Checkbox, Col, Empty, Form, Input, Modal, Row, Select, Spin, message } from 'antd';
import { Col, Form, Modal, Row, Select, message } from 'antd';
import type React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ApiError } from '../../api/client';
@ -7,6 +7,8 @@ import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thu
import { useAppDispatch, useAppSelector } from '../../store/store';
import type { ConsolidationCreate, LegalEntityBrief, Quarter, RelatedGroupOut } from '../../types/api';
import { membersFirst, preselectMembers } from './preselect';
import { PlusOutlined } from '@ant-design/icons';
import { CompanyMultiSelect } from './CompanyOptionRow';
type Props = {
open: boolean;
@ -30,13 +32,11 @@ export const NewConsolidationModal: React.FC<Props> = ({ open, onCancel, onOk })
const relatedReqRef = useRef(0);
const [submitting, setSubmitting] = useState(false);
const [legEntSearch, setLegEntSearch] = useState('');
const company_group_id = Form.useWatch('company_group_id', form);
const related_group_id = Form.useWatch('related_group_id', form);
const year = Form.useWatch('year', form);
const semester = Form.useWatch('quarter', form);
const legEntIds = Form.useWatch('legal_entity_ids', form);
const legEntIds = Form.useWatch('legal_entity_ids', form) ?? [];
const quarterOptions: { value: Quarter; label: string }[] = [
{ value: 'Q1', label: '1 квартал' },
@ -64,7 +64,6 @@ export const NewConsolidationModal: React.FC<Props> = ({ open, onCancel, onOk })
relatedReqRef.current++; // сменили ГК: ответ по старой ГСК больше не нужен
setLegEntities([]);
setLoadingEntities(false);
setLegEntSearch('');
legEntReqRef.current++;
};
@ -114,7 +113,6 @@ export const NewConsolidationModal: React.FC<Props> = ({ open, onCancel, onOk })
setLoadingSubgroups(false);
subgroupReqRef.current++;
clearLegEnts();
setLegEntSearch('');
};
const handleOk = async () => {
@ -142,25 +140,19 @@ export const NewConsolidationModal: React.FC<Props> = ({ open, onCancel, onOk })
onCancel();
};
const filteredLegEnts = useMemo(() => {
if (!legEntSearch) return legEntities;
const q = legEntSearch.toLowerCase();
return legEntities.filter((s) => s.name.toLowerCase().includes(q));
}, [legEntities, legEntSearch]);
return (
<Modal
title='Новая консолидация'
open={open}
onCancel={handleCancel}
onOk={handleOk}
okButtonProps={{ disabled: isSubmitDisabled }}
okButtonProps={{ disabled: isSubmitDisabled, icon: <PlusOutlined /> }}
okText='Создать'
cancelText='Отмена'
confirmLoading={submitting}
destroyOnHidden
width={640}>
<Form form={form} layout='vertical' preserve={false} initialValues={{ legal_entity_ids: [] }}>
<Form form={form} requiredMark={false} layout='vertical' preserve={false} initialValues={{ legal_entity_ids: [] }}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name='company_group_id' label='Группа компаний' rules={[{ required: true, message: 'Укажите ГК' }]}>
@ -205,78 +197,7 @@ export const NewConsolidationModal: React.FC<Props> = ({ open, onCancel, onOk })
{related_group_id && (
<Col span={24}>
<Form.Item name='legal_entity_ids' label='ЮЛ консолидации' rules={[{ required: true, message: 'Выберите хотя бы одно ЮЛ' }]}>
<Checkbox.Group style={{ width: '100%' }}>
<div
style={{
border: '1px solid #d9d9d9',
borderRadius: 8,
padding: 12,
width: '100%',
}}>
<Input.Search
placeholder='Поиск ЮЛ...'
allowClear
value={legEntSearch}
onChange={(e) => setLegEntSearch(e.target.value)}
style={{ marginBottom: 12 }}
/>
{loadingEntities ? (
<div style={{ textAlign: 'center', padding: 24 }}>
<Spin />
</div>
) : null}
{!loadingEntities && filteredLegEnts.length === 0 && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={legEntities.length === 0 ? 'Нет ЮЛ в ГК' : 'Ничего не найдено'}
/>
)}
{!loadingEntities && filteredLegEnts.length > 0 && (
<div
style={{
maxHeight: 200,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: 8,
}}>
{filteredLegEnts.map((s) => (
<Checkbox key={s.id} value={s.id} style={{ width: '100%', display: 'flex' }}>
<div
style={{
flex: 1,
justifySelf: 'flex-start',
display: 'flex',
justifyContent: 'space-between',
width: '100%',
gap: '30px',
}}>
<span style={{ display: 'inline-block' }}>{s.name}</span>
<span
style={{
display: 'inline-block',
color: 'rgb(102, 112, 133)',
fontSize: 11,
}}>
{s.inn}
</span>
</div>
</Checkbox>
))}
</div>
)}
{!loadingEntities && legEntities.length > 0 && (
<div style={{ marginTop: 8, display: 'flex', gap: 12 }}>
<span style={{ marginLeft: 'auto', color: '#8c8c8c' }}>
Выбрано: {legEntIds?.length ?? 0} / {legEntities.length}
</span>
</div>
)}
</div>
</Checkbox.Group>
<CompanyMultiSelect loading={loadingEntities} companies={legEntities} />
</Form.Item>
</Col>
)}