fix(web): справочники — выбранная ГК, счётчик ЮЛ, ошибки у полей (ревью #179, AURORA-1671)

- Страница ГК: снова выбирается первая ГК списка; список ЮЛ без выбранной ГК
  не запрашивается (иначе уходил запрос по всем ГК), «ЮЛ вне ГК» грузится без ГК.
- Выбранная ГК обновляется после перезагрузки списка ГК — счётчик ЮЛ в шапке
  и на вкладке после создания/переноса/вывода ЮЛ.
- Загрузка ГСК/ГСЗ: try/finally — ошибка не оставляет вечный спиннер.
- Форма ЮЛ: INVALID_INN/INVALID_KPP/INN_EXISTS/CRM_ID_EXISTS — у поля;
  КПП приводится к верхнему регистру при вводе.
- crm_id ГСК/ГСЗ — string | null; колонка «ID в CRM»; LE_PAGE вместо литерала;
  возвращён комментарий в фильтре шахматки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-27 00:00:47 +04:00
parent 630a616bf1
commit 3aad670510
11 changed files with 60 additions and 29 deletions

View File

@ -1,3 +1,4 @@
/* Теги выбранных значений в селектах: фильтр шахматки, синонимы ЮЛ */
.custom-select-selection-item .ant-select-selection-item { .custom-select-selection-item .ant-select-selection-item {
background-color: #ecfdf3 !important; background-color: #ecfdf3 !important;
color: #1f5f35 !important; color: #1f5f35 !important;

View File

@ -1,10 +1,12 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Button, Space } from 'antd'; import { Button, Space } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ApiError } from '../api/client';
import PageTitle from '../components/PageTitle'; import PageTitle from '../components/PageTitle';
import { createCatalogueJur } from '../services/api/directories/catalogue_jur'; import { createCatalogueJur } from '../services/api/directories/catalogue_jur';
import { createCompanyGroup, fetchRelatedGroups } from '../services/api/directories/company_groups'; import { createCompanyGroup, fetchRelatedGroups } from '../services/api/directories/company_groups';
import { createRelatedGroup } from '../services/api/directories/related-groups'; import { createRelatedGroup } from '../services/api/directories/related-groups';
import { setActiveCompanyGroup } from '../store/slices/catalogueJurSlice/catalogueJurSlice';
import { getCatalogueJurThunk } from '../store/slices/catalogueJurSlice/thunks'; import { getCatalogueJurThunk } from '../store/slices/catalogueJurSlice/thunks';
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';
@ -12,6 +14,7 @@ import type { RelatedGroupOut } from '../types/api';
import type { CatalogueJurFormProps } from '../types/catalogueJur/catalogueJur'; import type { CatalogueJurFormProps } from '../types/catalogueJur/catalogueJur';
import type { CreateCompanyGroupProps } from '../types/companyGroups/companyGroups'; import type { CreateCompanyGroupProps } from '../types/companyGroups/companyGroups';
import type { CreateEntityKind, RelatedGroupFormType } from '../types/relatedGroup/relatedGroup'; import type { CreateEntityKind, RelatedGroupFormType } from '../types/relatedGroup/relatedGroup';
import { useNotificationApi } from '../utils/useNotification';
import { CataloguGroupTabs, type CatalogueGroupTabKey } from '../widgets/CataloguGroupTabs/CataloguGroupTabs'; import { CataloguGroupTabs, type CatalogueGroupTabKey } from '../widgets/CataloguGroupTabs/CataloguGroupTabs';
import { CompanyGroupHeader } from '../widgets/CatalogueGroupHeader/CatalogueGroupHeader'; import { CompanyGroupHeader } from '../widgets/CatalogueGroupHeader/CatalogueGroupHeader';
import { CatalogueJurForm } from '../widgets/CatalogueJurForm/CatalogueJurForm'; import { CatalogueJurForm } from '../widgets/CatalogueJurForm/CatalogueJurForm';
@ -24,6 +27,7 @@ import { RelatedGroupTable } from '../widgets/RelatedGroupTable/RelatedGroupTabl
export function CatalogueGroup() { export function CatalogueGroup() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const notification = useNotificationApi();
const isGettingArchive = useAppSelector((state) => state.catalogue_jur.isGettingArchive); const isGettingArchive = useAppSelector((state) => state.catalogue_jur.isGettingArchive);
@ -35,6 +39,7 @@ export function CatalogueGroup() {
const [gszData, setGszData] = useState<RelatedGroupOut[]>([]); const [gszData, setGszData] = useState<RelatedGroupOut[]>([]);
const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup); const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup);
const relatedGroupFilter = useAppSelector((state) => state.catalogue_jur.relatedGroupFilter); const relatedGroupFilter = useAppSelector((state) => state.catalogue_jur.relatedGroupFilter);
const rawRows = useAppSelector((state) => state.company_groups.raw_rows);
async function createJurHandler(values: CatalogueJurFormProps) { async function createJurHandler(values: CatalogueJurFormProps) {
await createCatalogueJur(values); await createCatalogueJur(values);
@ -68,19 +73,33 @@ export function CatalogueGroup() {
async function getRelatedGroupsHandler() { async function getRelatedGroupsHandler() {
if (activeCompanyGroup?.id) { if (activeCompanyGroup?.id) {
setRelatedGroupLoading(true); setRelatedGroupLoading(true);
try {
const res = await fetchRelatedGroups(activeCompanyGroup?.id); const res = await fetchRelatedGroups(activeCompanyGroup?.id);
const gsks = res.filter((elem) => elem.kind === 'gsk'); const gsks = res.filter((elem) => elem.kind === 'gsk');
const gszs = res.filter((elem) => elem.kind === 'gsz'); const gszs = res.filter((elem) => elem.kind === 'gsz');
setGskData(gsks); setGskData(gsks);
setGszData(gszs); setGszData(gszs);
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
setRelatedGroupLoading(false); setRelatedGroupLoading(false);
} }
} }
}
useEffect(() => { useEffect(() => {
getRelatedGroupsHandler(); getRelatedGroupsHandler();
}, [activeCompanyGroup]); }, [activeCompanyGroup]);
// Без выбранной ГК таблица ЮЛ не грузится — выбираем первую ГК списка
useEffect(() => {
if (!activeCompanyGroup && rawRows?.length) {
dispatch(setActiveCompanyGroup(rawRows[0]));
}
}, [rawRows, activeCompanyGroup, dispatch]);
useEffect(() => { useEffect(() => {
setActiveTab('legal_entities'); setActiveTab('legal_entities');
}, [relatedGroupFilter]); }, [relatedGroupFilter]);

View File

@ -7,9 +7,10 @@ import type {
MoveCatalogueJurProps, MoveCatalogueJurProps,
WithdrawCatalogueJurProps, WithdrawCatalogueJurProps,
} from '../../../types/catalogueJur/catalogueJur'; } from '../../../types/catalogueJur/catalogueJur';
import { LE_PAGE } from './company_groups';
export async function fetchCatalogueJur(params: GetCatalogueJurPropsI) { export async function fetchCatalogueJur(params: GetCatalogueJurPropsI) {
const res = await api.get<Page<ICatalogueJur>>('/directories/legal-entities', { limit: 200, offset: 0, ...params }); const res = await api.get<Page<ICatalogueJur>>('/directories/legal-entities', { limit: LE_PAGE, offset: 0, ...params });
return res; return res;
} }

View File

@ -1,6 +1,7 @@
import { type PayloadAction, createSlice } from '@reduxjs/toolkit'; import { type PayloadAction, createSlice } from '@reduxjs/toolkit';
import type { RelatedGroupOut } from '../../../types/api'; import type { RelatedGroupOut } from '../../../types/api';
import type { ICompanyGroup } from '../../../types/companyGroups/companyGroups'; import type { ICompanyGroup } from '../../../types/companyGroups/companyGroups';
import { getCompanyGroupsThunk } from '../companyGroupsSlice/thunks';
import { getCatalogueJurThunk } from './thunks'; import { getCatalogueJurThunk } from './thunks';
import type { CatalogueJurSliceType } from './types'; import type { CatalogueJurSliceType } from './types';
@ -43,6 +44,12 @@ const catalogueJurSlice = createSlice({
.addCase(getCatalogueJurThunk.rejected, (state, action) => { .addCase(getCatalogueJurThunk.rejected, (state, action) => {
state.error = action.payload ?? 'Неизвестная ошибка'; state.error = action.payload ?? 'Неизвестная ошибка';
state.loading = false; state.loading = false;
})
// Выбранная ГК — копия строки списка ГК: после перезагрузки списка берём свежую (счётчик ЮЛ в шапке и вкладке)
.addCase(getCompanyGroupsThunk.fulfilled, (state, action) => {
if (state.activeCompanyGroup?.id) {
state.activeCompanyGroup = action.payload.find((elem) => elem.id === state.activeCompanyGroup?.id) ?? null;
}
}); });
}, },
}); });

View File

@ -375,7 +375,7 @@ export interface ConsolidationCreate {
export interface RelatedGroupOut { export interface RelatedGroupOut {
id: string; id: string;
code: string; code: string;
crm_id: string; crm_id: string | null;
company_group_id: string; company_group_id: string;
kind: 'gsk' | 'gsz'; kind: 'gsk' | 'gsz';
name: string; name: string;
@ -390,7 +390,6 @@ export interface RelatedGroupMemberOut {
} }
export interface RelatedGroupDetail extends RelatedGroupOut { export interface RelatedGroupDetail extends RelatedGroupOut {
members: RelatedGroupMemberOut[]; members: RelatedGroupMemberOut[];
crm_id: string;
} }
export interface LegalEntityBrief { export interface LegalEntityBrief {
id: string; id: string;

View File

@ -1,7 +1,7 @@
export type RelatedGroupFormType = { export type RelatedGroupFormType = {
name: string; name: string;
created_date: string | null; created_date: string | null;
crm_id: string; crm_id: string | null;
kind?: 'gsk' | 'gsz'; kind?: 'gsk' | 'gsz';
company_group_id?: string; company_group_id?: string;
}; };

View File

@ -17,6 +17,14 @@ type Props = {
jurId?: string; jurId?: string;
}; };
// Ошибки сервера, относящиеся к одному полю, показываем у этого поля
const FIELD_BY_ERROR_CODE: Record<string, keyof CatalogueJurFormProps> = {
INN_EXISTS: 'inn',
INVALID_INN: 'inn',
INVALID_KPP: 'kpp',
CRM_ID_EXISTS: 'crm_id',
};
export const CatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, jurId }) => { export const CatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, jurId }) => {
const [form] = Form.useForm<CatalogueJurFormProps>(); const [form] = Form.useForm<CatalogueJurFormProps>();
const notification = useNotificationApi(); const notification = useNotificationApi();
@ -116,21 +124,9 @@ export const CatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, jurId
}); });
} catch (e) { } catch (e) {
if (e instanceof ApiError) { if (e instanceof ApiError) {
if (e.code === 'INN_EXISTS') { const field = e.code ? FIELD_BY_ERROR_CODE[e.code] : undefined;
form.setFields([ if (field) {
{ form.setFields([{ name: field, errors: [e.message] }]);
name: 'inn',
errors: [e.message],
},
]);
}
if (e.code === 'CRM_ID_EXISTS') {
form.setFields([
{
name: 'crm_id',
errors: [e.message],
},
]);
} }
notification.error({ description: e.message }); notification.error({ description: e.message });
} }
@ -194,7 +190,11 @@ export const CatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, jurId
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item name='kpp' label='КПП' rules={[{ pattern: /^[0-9]{4}[0-9A-Z]{2}[0-9]{3}$/, message: 'Некорректный формат' }]}> <Form.Item
name='kpp'
label='КПП'
normalize={(value?: string) => value?.toUpperCase()}
rules={[{ pattern: /^[0-9]{4}[0-9A-Z]{2}[0-9]{3}$/, message: 'Некорректный формат' }]}>
<Input placeholder='000000000' maxLength={9} disabled={type === 'ИП'} style={{ width: '100%' }} /> <Input placeholder='000000000' maxLength={9} disabled={type === 'ИП'} style={{ width: '100%' }} />
</Form.Item> </Form.Item>
</Col> </Col>

View File

@ -128,7 +128,10 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur
} }
useEffect(() => { useEffect(() => {
// «ЮЛ вне ГК» грузится без выбранной ГК; список ГК — только по выбранной (без неё запрос ушёл бы по всем ГК)
if (outside_group || activeCompanyGroup?.id) {
getCatalogueJur(); getCatalogueJur();
}
}, [isGettingArchive, activeCompanyGroup, outside_group]); }, [isGettingArchive, activeCompanyGroup, outside_group]);
const extraColumns: ColumnsType<ICatalogueJur> = useMemo( const extraColumns: ColumnsType<ICatalogueJur> = useMemo(

View File

@ -15,6 +15,7 @@ interface ConsolidationChessTableFilterI {
credit: string[]; credit: string[];
onCreditChange: (val: string[]) => void; onCreditChange: (val: string[]) => void;
allCompanies: MatrixCompany[]; allCompanies: MatrixCompany[];
/** Год и квартал версии — только для показа: версия задаётся адресом. */
year?: number; year?: number;
quarter?: Quarter; quarter?: Quarter;
} }

View File

@ -42,7 +42,7 @@ export const RelatedGroupForm: React.FC<Props> = ({ open, onCancel, onOk, relate
form.setFieldsValue({ form.setFieldsValue({
crm_id: data?.crm_id, crm_id: data?.crm_id,
name: data?.name, name: data?.name,
created_date: data?.created_date ?? (relatedGroupId ? null : dayjs().format('YYYY-MM-DD')), created_date: data?.created_date ?? null,
company_group_id: data?.company_group_id, company_group_id: data?.company_group_id,
}); });
} catch (e) { } catch (e) {

View File

@ -15,7 +15,7 @@ export const relateGroupConfig: TableConfig<RelatedGroupOut> = {
}, },
{ {
key: 'crm_id', key: 'crm_id',
title: 'CRM ID', title: 'ID в CRM',
}, },
{ {
key: 'name', key: 'name',