121 lines
3.2 KiB
TypeScript
121 lines
3.2 KiB
TypeScript
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>
|
||
);
|
||
};
|