fix: выравнивание ИНН при создании новой консолидации, + и убраны звёзды обязательности
This commit is contained in:
parent
a7600444e6
commit
2e75de33cc
120
web/src/widgets/NewConsoleModal/CompanyOptionRow.tsx
Normal file
120
web/src/widgets/NewConsoleModal/CompanyOptionRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user