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 {
background-color: #ecfdf3 !important;
color: #1f5f35 !important;

View File

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

View File

@ -7,9 +7,10 @@ import type {
MoveCatalogueJurProps,
WithdrawCatalogueJurProps,
} from '../../../types/catalogueJur/catalogueJur';
import { LE_PAGE } from './company_groups';
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;
}

View File

@ -1,6 +1,7 @@
import { type PayloadAction, createSlice } from '@reduxjs/toolkit';
import type { RelatedGroupOut } from '../../../types/api';
import type { ICompanyGroup } from '../../../types/companyGroups/companyGroups';
import { getCompanyGroupsThunk } from '../companyGroupsSlice/thunks';
import { getCatalogueJurThunk } from './thunks';
import type { CatalogueJurSliceType } from './types';
@ -43,6 +44,12 @@ const catalogueJurSlice = createSlice({
.addCase(getCatalogueJurThunk.rejected, (state, action) => {
state.error = action.payload ?? 'Неизвестная ошибка';
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 {
id: string;
code: string;
crm_id: string;
crm_id: string | null;
company_group_id: string;
kind: 'gsk' | 'gsz';
name: string;
@ -390,7 +390,6 @@ export interface RelatedGroupMemberOut {
}
export interface RelatedGroupDetail extends RelatedGroupOut {
members: RelatedGroupMemberOut[];
crm_id: string;
}
export interface LegalEntityBrief {
id: string;

View File

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

View File

@ -17,6 +17,14 @@ type Props = {
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 }) => {
const [form] = Form.useForm<CatalogueJurFormProps>();
const notification = useNotificationApi();
@ -116,21 +124,9 @@ export const CatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, jurId
});
} catch (e) {
if (e instanceof ApiError) {
if (e.code === 'INN_EXISTS') {
form.setFields([
{
name: 'inn',
errors: [e.message],
},
]);
}
if (e.code === 'CRM_ID_EXISTS') {
form.setFields([
{
name: 'crm_id',
errors: [e.message],
},
]);
const field = e.code ? FIELD_BY_ERROR_CODE[e.code] : undefined;
if (field) {
form.setFields([{ name: field, errors: [e.message] }]);
}
notification.error({ description: e.message });
}
@ -194,7 +190,11 @@ export const CatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, jurId
</Form.Item>
</Col>
<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%' }} />
</Form.Item>
</Col>

View File

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

View File

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

View File

@ -42,7 +42,7 @@ export const RelatedGroupForm: React.FC<Props> = ({ open, onCancel, onOk, relate
form.setFieldsValue({
crm_id: data?.crm_id,
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,
});
} catch (e) {

View File

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