fix(web): «Новая консолидация» — «+ Создать» от двух ЮЛ, строка ЮЛ — флажок antd (AURORA-1589, PR #166)

Сервер отвечает 422 PERIMETER_TOO_SMALL при одном ЮЛ — кнопка неактивна раньше, у поля
«Выберите не меньше двух ЮЛ». Строка ЮЛ — сам Checkbox (label на всю строку) вместо
div role="checkbox" (biome useSemanticElements). Предупреждение браузера при начатой форме.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-23 01:15:55 +04:00
parent d71c46e846
commit 2fa672a2fb
2 changed files with 37 additions and 40 deletions

View File

@ -1,52 +1,36 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { LegalEntityBrief } from '../../types/api';
import { Checkbox, Empty, Input, Spin } from 'antd'; import { Checkbox, Empty, Input, Spin } from 'antd';
import { memo, useCallback, useMemo, useState } from 'react';
import type { LegalEntityBrief } from '../../types/api';
type RowProps = { type RowProps = {
entity: LegalEntityBrief; entity: LegalEntityBrief;
checked: boolean; checked: boolean;
onToggle: (id: string) => void; onToggle: (id: string) => void;
}; };
// Вся строка — флажок antd (<label>): клик по наименованию или ИНН переключает, клавиатура — как у флажка.
// Подпись растянута на ширину строки: наименование с многоточием слева, ИНН — колонкой справа.
const CompanyOptionRow = memo(({ entity, checked, onToggle }: RowProps) => ( const CompanyOptionRow = memo(({ entity, checked, onToggle }: RowProps) => (
<div <Checkbox
role='checkbox' checked={checked}
aria-checked={checked} onChange={() => onToggle(entity.id)}
tabIndex={0} styles={{
onClick={() => onToggle(entity.id)} root: {
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', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 8, width: '100%',
flex: 1,
minWidth: 0, minWidth: 0,
overflow: 'hidden', padding: '10px 8px',
}}> borderRadius: 4,
<Checkbox checked={checked} tabIndex={-1} style={{ pointerEvents: 'none' }} /> borderBottom: '1px solid rgba(0, 0, 0, 0.05)',
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{entity.name}</span> boxSizing: 'border-box',
},
label: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flex: 1, minWidth: 0 },
}}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }} title={entity.name}>
{entity.name}
</span> </span>
<span style={{ color: '#8c8c8c', flexShrink: 0 }}>{entity.inn}</span> <span style={{ color: '#8c8c8c', flexShrink: 0 }}>{entity.inn}</span>
</div> </Checkbox>
)); ));
type CompanyMultiSelectProps = { type CompanyMultiSelectProps = {

View File

@ -1,14 +1,16 @@
import { PlusOutlined } from '@ant-design/icons';
import { Col, Form, Modal, Row, Select, message } from 'antd'; import { Col, Form, Modal, Row, Select, message } from 'antd';
import type React from 'react'; import type React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { ApiError } from '../../api/client'; import { ApiError } from '../../api/client';
import { useBeforeUnload } from '../../hooks/useBeforeUnload';
import { fetchAllLegalEntities, fetchRelatedGroup, fetchRelatedGroups } from '../../services/api/directories/company_groups'; import { fetchAllLegalEntities, fetchRelatedGroup, fetchRelatedGroups } from '../../services/api/directories/company_groups';
import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks'; import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks';
import { useAppDispatch, useAppSelector } from '../../store/store'; import { useAppDispatch, useAppSelector } from '../../store/store';
import type { ConsolidationCreate, LegalEntityBrief, Quarter, RelatedGroupOut } from '../../types/api'; import type { ConsolidationCreate, LegalEntityBrief, Quarter, RelatedGroupOut } from '../../types/api';
import { membersFirst, preselectMembers } from './preselect'; import { MIN_PERIMETER } from '../../utils/consolidations';
import { PlusOutlined } from '@ant-design/icons';
import { CompanyMultiSelect } from './CompanyOptionRow'; import { CompanyMultiSelect } from './CompanyOptionRow';
import { membersFirst, preselectMembers } from './preselect';
type Props = { type Props = {
open: boolean; open: boolean;
@ -50,7 +52,10 @@ export const NewConsolidationModal: React.FC<Props> = ({ open, onCancel, onOk })
} }
}, [open, raw_rows.length, loading, dispatch]); }, [open, raw_rows.length, loading, dispatch]);
const isSubmitDisabled = !company_group_id || !related_group_id || !year || !semester || !legEntIds?.length; // не меньше двух ЮЛ: иначе сервер ответит 422 PERIMETER_TOO_SMALL
const isSubmitDisabled = !company_group_id || !related_group_id || !year || !semester || legEntIds.length < MIN_PERIMETER;
// начатая форма — несохранённые правки: браузер предупредит при закрытии вкладки
useBeforeUnload(open && !!(company_group_id || related_group_id || year || semester));
const yearOptions = useMemo(() => { const yearOptions = useMemo(() => {
const y = new Date().getFullYear(); const y = new Date().getFullYear();
@ -196,7 +201,15 @@ export const NewConsolidationModal: React.FC<Props> = ({ open, onCancel, onOk })
{/* ROW 3 - full width block, only after subgroup is selected */} {/* ROW 3 - full width block, only after subgroup is selected */}
{related_group_id && ( {related_group_id && (
<Col span={24}> <Col span={24}>
<Form.Item name='legal_entity_ids' label='ЮЛ консолидации' rules={[{ required: true, message: 'Выберите хотя бы одно ЮЛ' }]}> <Form.Item
name='legal_entity_ids'
label='ЮЛ консолидации'
rules={[
{
validator: (_, v: string[] | undefined) =>
(v?.length ?? 0) >= MIN_PERIMETER ? Promise.resolve() : Promise.reject(new Error('Выберите не меньше двух ЮЛ')),
},
]}>
<CompanyMultiSelect loading={loadingEntities} companies={legEntities} /> <CompanyMultiSelect loading={loadingEntities} companies={legEntities} />
</Form.Item> </Form.Item>
</Col> </Col>