diff --git a/src/App.tsx b/src/App.tsx index 2adc581..24faf7a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import './App.css'; import { Layout } from 'antd'; +import { App as AntApp } from 'antd'; import { Provider } from 'react-redux'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { AuthGate } from './auth/AuthGate'; @@ -24,30 +25,32 @@ import { Sidebar } from './widgets/Sidebar/Sidebar'; function App() { return ( - - - - - - - } /> - } /> - - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - - - - - + + + + + + + + } /> + } /> + + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + + + + + + ); } diff --git a/src/api/client.ts b/src/api/client.ts index a3e5806..e6522d2 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -28,6 +28,17 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } +function buildQuery(params?: object): string { + if (!params) return ''; + const search = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) continue; + search.append(key, String(value)); + } + const qs = search.toString(); + return qs ? `?${qs}` : ''; +} + /** * Тело ошибки бэкенда — `{"detail": …}` в одной из трёх форм: * строка (HTTPException) → message; объект `{code, message}` (доменная ошибка) → code + message; @@ -127,7 +138,7 @@ function withJsonBody(method: string, body: unknown): RequestInit { } export const api = { - get: (path: string) => apiFetch(path), + get: (path: string, params?: object) => apiFetch(`${path}${buildQuery(params)}`), post: (path: string, body?: unknown) => apiFetch(path, withJsonBody('POST', body)), patch: (path: string, body?: unknown) => apiFetch(path, withJsonBody('PATCH', body)), put: (path: string, body?: unknown) => apiFetch(path, withJsonBody('PUT', body)), diff --git a/src/components/TableTag/presets.tsx b/src/components/TableTag/presets.tsx index f35feab..1846879 100644 --- a/src/components/TableTag/presets.tsx +++ b/src/components/TableTag/presets.tsx @@ -39,4 +39,9 @@ export const TAG_PRESETS = { borderColor: 'rgba(245, 158, 11, 0.25)', color: 'rgb(146, 64, 14)', }, + gray: { + color: 'rgb(71, 84, 103)', + bgColor: 'rgb(248, 250, 252)', + borderColor: 'rgba(15, 23, 42, 0.08)', + }, } as const; diff --git a/src/hooks/useVirtualList.tsx b/src/hooks/useVirtualList.tsx new file mode 100644 index 0000000..d019b08 --- /dev/null +++ b/src/hooks/useVirtualList.tsx @@ -0,0 +1,46 @@ +import { useCallback, useMemo, useRef, useState } from 'react'; + +type Options = { + /** высота одного элемента в px (фиксированная) */ + itemHeight: number; + /** высота контейнера в px (видимая область) */ + containerHeight: number; + /** сколько элементов рендерить дополнительно сверху/снизу */ + overscan?: number; +}; + +export function useVirtualList(items: T[], options: Options) { + const { itemHeight, containerHeight, overscan = 5 } = options; + + const [scrollTop, setScrollTop] = useState(0); + const ref = useRef(null); + + const totalHeight = items.length * itemHeight; + + const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan); + + const endIndex = Math.min(items.length, Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan); + + const visibleItems = useMemo( + () => + items.slice(startIndex, endIndex).map((item, i) => ({ + item, + index: startIndex + i, + })), + [items, startIndex, endIndex], + ); + + const offsetY = startIndex * itemHeight; + + const onScroll = useCallback((e: React.UIEvent) => { + setScrollTop(e.currentTarget.scrollTop); + }, []); + + return { + ref, + onScroll, + visibleItems, + totalHeight, + offsetY, + }; +} diff --git a/src/index.tsx b/src/index.tsx index 3697fda..207a715 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,14 @@ import { createRoot } from 'react-dom/client'; import './index.css'; import { ConfigProvider } from 'antd'; +import locale from 'antd/locale/ru_RU'; +import dayjs from 'dayjs'; import App from './App'; +import 'dayjs/locale/ru'; + +dayjs.locale('ru'); + const rootElement = document.getElementById('root'); if (!rootElement) { throw new Error('Не найден корневой элемент #root'); @@ -11,6 +17,7 @@ if (!rootElement) { createRoot(rootElement).render( // ГК и организации; + const dispatch = useAppDispatch(); + const notification = useNotificationApi(); + + const isGettingArchive = useAppSelector((state) => state.catalogue_jur.isGettingArchive); + + const [entityCreateModalOpen, setEntityCreateModalOpen] = useState(false); + const [kindForm, setKindForm] = useState(null); + const [relatedGroupLoading, setRelatedGroupLoading] = useState(false); + const [activeTab, setActiveTab] = useState('legal_entities'); + const [gskData, setGskData] = useState([]); + const [gszData, setGszData] = useState([]); + const activeCompanyGroup = useAppSelector((state) => state.company_groups.activeCompanyGroup); + const relatedGroupFilter = useAppSelector((state) => state.catalogue_jur.relatedGroupFilter); + const rawRows = useAppSelector((state) => state.company_groups.raw_rows); + + async function createJurHandler(values: CatalogueJurFormProps) { + try { + await createCatalogueJur(values); + dispatch(getCompanyGroupsThunk()); + dispatch( + getCatalogueJurThunk({ + closed: isGettingArchive, + outside_group: false, + company_group_id: activeCompanyGroup?.id, + }), + ); + setKindForm(null); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } + } + async function createRelatedGroupHandler(values: RelatedGroupFormType) { + if (values.company_group_id) { + try { + await createRelatedGroup(values.company_group_id, { + ...values, + company_group_id: undefined, + }); + getRelatedGroupsHandler(); + setKindForm(null); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } + } + } + async function createCompanyGroupHandler(values: CreateCompanyGroupProps) { + try { + await createCompanyGroup(values); + dispatch(getCompanyGroupsThunk()); + getRelatedGroupsHandler(); + setKindForm(null); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } + } + + async function getRelatedGroupsHandler() { + if (activeCompanyGroup?.id) { + setRelatedGroupLoading(true); + 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); + } + } + } + + useEffect(() => { + getRelatedGroupsHandler(); + }, [activeCompanyGroup]); + + useEffect(() => { + setActiveTab('legal_entities'); + }, [relatedGroupFilter]); + + useEffect(() => { + if (!activeCompanyGroup && rawRows?.length) { + dispatch(setActiveCompanyGroup(rawRows?.[0])); + } + }, [rawRows, activeCompanyGroup]); + + return ( +
+ + + + +
+
+ +
+ +
+ + {activeTab === 'legal_entities' && ( + setActiveTab(val)} gskData={gskData} gszData={gszData} /> + } + /> + )} + {(activeTab === 'gsk' || activeTab === 'gsz') && ( + getRelatedGroupsHandler()} + tabsComponent={ + setActiveTab(val)} gskData={gskData} gszData={gszData} /> + } + loading={relatedGroupLoading} + /> + )} +
+
+ { + setEntityCreateModalOpen(false); + }} + onSelect={(kind) => { + setKindForm(kind); + setEntityCreateModalOpen(false); + }} + /> + {kindForm === 'ul' && ( + { + setKindForm(null); + }} + onOk={async (values) => { + await createJurHandler(values); + }} + /> + )} + {(kindForm === 'gsk' || kindForm === 'gsz') && ( + { + setKindForm(null); + }} + onOk={async (values) => { + await createRelatedGroupHandler(values); + }} + /> + )} + {kindForm === 'gk' && ( + { + setKindForm(null); + }} + onOk={async (values) => { + await createCompanyGroupHandler(values); + }} + /> + )} +
+ ); } diff --git a/src/layouts/CatalogueJur.tsx b/src/layouts/CatalogueJur.tsx index a298cfd..3b2ac3b 100644 --- a/src/layouts/CatalogueJur.tsx +++ b/src/layouts/CatalogueJur.tsx @@ -1,3 +1,69 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { Button, Space } from 'antd'; +import { useState } from 'react'; +import { ApiError } from '../api/client'; +import PageTitle from '../components/PageTitle'; +import { createCatalogueJur } from '../services/api/directories/catalogue_jur'; +import { getCatalogueJurThunk } from '../store/slices/catalogueJurSlice/thunks'; +import { useAppDispatch, useAppSelector } from '../store/store'; +import type { CatalogueJurFormProps } from '../types/catalogueJur/catalogueJur'; +import { useNotificationApi } from '../utils/useNotification'; +import { CatalogueJurForm } from '../widgets/CatalogueJurForm/CatalogueJurForm'; +import { CatalogueJurTable } from '../widgets/CatalogueJurTable/CatalogueJurTable'; + export function CatalogueJur() { - return

ЮЛ вне ГК

; + const dispatch = useAppDispatch(); + const notification = useNotificationApi(); + + const isGettingArchive = useAppSelector((state) => state.catalogue_jur.isGettingArchive); + + const [createFormOpen, setCreateFormOpen] = useState(false); + const outside_group = true; + async function createJurHandler(values: CatalogueJurFormProps) { + try { + await createCatalogueJur(values); + dispatch( + getCatalogueJurThunk({ + closed: isGettingArchive, + outside_group: outside_group, + }), + ); + setCreateFormOpen(false); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } + } + + return ( +
+ + + + + + {createFormOpen && ( + { + setCreateFormOpen(false); + }} + onOk={async (values) => { + await createJurHandler(values); + }} + /> + )} +
+ ); } diff --git a/src/services/api/directories/catalogue_jur.ts b/src/services/api/directories/catalogue_jur.ts new file mode 100644 index 0000000..9b1d30a --- /dev/null +++ b/src/services/api/directories/catalogue_jur.ts @@ -0,0 +1,34 @@ +import { api } from '../../../api/client'; +import type { GetCatalogueJurPropsI } from '../../../store/slices/catalogueJurSlice/types'; +import type { Page } from '../../../types/api'; +import type { + CatalogueJurFormProps, + ICatalogueJur, + MoveCatalogueJurProps, + WithdrawCatalogueJurProps, +} from '../../../types/catalogueJur/catalogueJur'; +import { LE_PAGE } from './company_groups'; + +export async function fetchCatalogueJur(params: GetCatalogueJurPropsI) { + const res = await api.get>('/directories/legal-entities', { limit: LE_PAGE, offset: 0, ...params }); + + return res; +} + +export async function fetchOneCatalogueJur(catalogue_jur: string): Promise { + const res = await api.get(`/directories/legal-entities/${catalogue_jur}`); + return res; +} + +export function moveCatalogueJur(entityId: string, body: MoveCatalogueJurProps): Promise { + return api.post(`/directories/legal-entities/${entityId}/move`, body); +} +export function withdrawCatalogueJur(entityId: string, body: WithdrawCatalogueJurProps): Promise { + return api.post(`/directories/legal-entities/${entityId}/withdraw`, body); +} +export function createCatalogueJur(body: CatalogueJurFormProps): Promise { + return api.post('/directories/legal-entities', body); +} +export function editCatalogueJur(entityId: string, body: CatalogueJurFormProps): Promise { + return api.patch(`/directories/legal-entities/${entityId}`, body); +} diff --git a/src/services/api/directories/company_groups.ts b/src/services/api/directories/company_groups.ts index be04b11..9ca3f26 100644 --- a/src/services/api/directories/company_groups.ts +++ b/src/services/api/directories/company_groups.ts @@ -1,9 +1,9 @@ import { api } from '../../../api/client'; import type { LegalEntityBrief, Page, RelatedGroupDetail, RelatedGroupOut } from '../../../types/api'; -import type { ICompanyGroupOut } from '../../../types/companyGroups/companyGroups'; +import type { CreateCompanyGroupProps, ICompanyGroupOut } from '../../../types/companyGroups/companyGroups'; const CG_PAGE = 200; -const LE_PAGE = 200; +export const LE_PAGE = 200; /** Все видимые ГК — дочитываем страницы, пока не получим total. */ export async function fetchCompanyGroups(): Promise { @@ -36,3 +36,7 @@ export async function fetchAllLegalEntities(groupId: string): Promise api.get(`/directories/legal-entities/${id}`); + +export function createCompanyGroup(body: CreateCompanyGroupProps): Promise { + return api.post('/directories/company-groups', body); +} diff --git a/src/services/api/directories/related-groups.ts b/src/services/api/directories/related-groups.ts new file mode 100644 index 0000000..e25fa6f --- /dev/null +++ b/src/services/api/directories/related-groups.ts @@ -0,0 +1,11 @@ +import { api } from '../../../api/client'; +import type { RelatedGroupFormType } from '../../../types/relatedGroup/relatedGroup'; + +export function editRelatedGroup(groupId: string, body: RelatedGroupFormType): Promise { + return api.patch(`/directories/related-groups/${groupId}`, body); +} +export function createRelatedGroup(companyGroupId: string, body: RelatedGroupFormType): Promise { + return api.post(`/directories/company-groups/${companyGroupId}/related-groups`, body); +} + +export const deleteRelatedGroup = (id: string) => api.del(`/directories/related-groups/${id}`); diff --git a/src/store/slices/catalogueJurSlice/catalogueJurSlice.ts b/src/store/slices/catalogueJurSlice/catalogueJurSlice.ts new file mode 100644 index 0000000..7a14e25 --- /dev/null +++ b/src/store/slices/catalogueJurSlice/catalogueJurSlice.ts @@ -0,0 +1,44 @@ +import { type PayloadAction, createSlice } from '@reduxjs/toolkit'; +import type { RelatedGroupOut } from '../../../types/api'; +import { getCatalogueJurThunk } from './thunks'; +import type { CatalogueJurSliceType } from './types'; + +const initialState: CatalogueJurSliceType = { + raw_rows: [], + total_groups: 0, + loading: false, + error: null, + isGettingArchive: false, + relatedGroupFilter: null, +}; + +const catalogueJurSlice = createSlice({ + name: 'catalogue_jur', + initialState, + reducers: { + setIsGettingArchive(state, action: PayloadAction) { + state.isGettingArchive = action.payload; + }, + setRelatedGroupFilter(state, action: PayloadAction) { + state.relatedGroupFilter = action.payload; + }, + }, + extraReducers: (builder) => { + builder + .addCase(getCatalogueJurThunk.pending, (state, _) => { + state.loading = true; + state.error = null; + }) + .addCase(getCatalogueJurThunk.fulfilled, (state, action) => { + state.raw_rows = action.payload.items; + state.total_groups = action.payload.total; + state.loading = false; + }) + .addCase(getCatalogueJurThunk.rejected, (state, action) => { + state.error = action.payload ?? 'Неизвестная ошибка'; + state.loading = false; + }); + }, +}); +export const { setIsGettingArchive, setRelatedGroupFilter } = catalogueJurSlice.actions; +export default catalogueJurSlice.reducer; diff --git a/src/store/slices/catalogueJurSlice/thunks.ts b/src/store/slices/catalogueJurSlice/thunks.ts new file mode 100644 index 0000000..5bf7c9b --- /dev/null +++ b/src/store/slices/catalogueJurSlice/thunks.ts @@ -0,0 +1,16 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { fetchCatalogueJur } from '../../../services/api/directories/catalogue_jur'; +import type { Page } from '../../../types/api'; +import type { ICatalogueJur } from '../../../types/catalogueJur/catalogueJur'; +import type { GetCatalogueJurPropsI } from './types'; + +export const getCatalogueJurThunk = createAsyncThunk, GetCatalogueJurPropsI, { rejectValue: string }>( + 'catalogue_Jur/getAll', + async (args: GetCatalogueJurPropsI, thunkApi) => { + try { + return await fetchCatalogueJur(args); + } catch (e) { + return thunkApi.rejectWithValue(e instanceof Error ? e.message : 'Ошибка'); + } + }, +); diff --git a/src/store/slices/catalogueJurSlice/types.ts b/src/store/slices/catalogueJurSlice/types.ts new file mode 100644 index 0000000..0ed8c2b --- /dev/null +++ b/src/store/slices/catalogueJurSlice/types.ts @@ -0,0 +1,17 @@ +import type { RelatedGroupOut } from '../../../types/api'; +import type { ICatalogueJur } from '../../../types/catalogueJur/catalogueJur'; + +export interface CatalogueJurSliceType { + total_groups: number; + raw_rows: ICatalogueJur[]; + loading: boolean; + error: string | null; + isGettingArchive: boolean; + relatedGroupFilter?: RelatedGroupOut | null; +} + +export interface GetCatalogueJurPropsI { + closed?: boolean; + outside_group?: boolean; + company_group_id?: string; +} diff --git a/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts b/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts index 0ad437a..c298b6a 100644 --- a/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts +++ b/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts @@ -1,4 +1,5 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { type PayloadAction, createSlice } from '@reduxjs/toolkit'; +import type { ICompanyGroup } from '../../../types/companyGroups/companyGroups'; import { getCompanyGroupsThunk } from './thunks'; import type { CompanyGroupSliceType } from './types'; @@ -7,12 +8,17 @@ const initialState: CompanyGroupSliceType = { total_groups: 0, loading: false, error: null, + activeCompanyGroup: null, }; const comGroupsSlice = createSlice({ name: 'company_groups', initialState, - reducers: {}, + reducers: { + setActiveCompanyGroup(state, action: PayloadAction) { + state.activeCompanyGroup = action.payload; + }, + }, extraReducers: (builder) => { builder .addCase(getCompanyGroupsThunk.pending, (state) => { @@ -21,6 +27,10 @@ const comGroupsSlice = createSlice({ }) .addCase(getCompanyGroupsThunk.fulfilled, (state, action) => { state.raw_rows = action.payload; + if (state.activeCompanyGroup?.id) { + const newActiveCompanyGroup = action.payload.find((elem) => elem.id === state.activeCompanyGroup?.id); + state.activeCompanyGroup = newActiveCompanyGroup; + } state.total_groups = action.payload.length; state.loading = false; }) @@ -30,5 +40,5 @@ const comGroupsSlice = createSlice({ }); }, }); - +export const { setActiveCompanyGroup } = comGroupsSlice.actions; export default comGroupsSlice.reducer; diff --git a/src/store/slices/companyGroupsSlice/types.ts b/src/store/slices/companyGroupsSlice/types.ts index 5f35cfa..7a49aa3 100644 --- a/src/store/slices/companyGroupsSlice/types.ts +++ b/src/store/slices/companyGroupsSlice/types.ts @@ -5,4 +5,5 @@ export interface CompanyGroupSliceType { raw_rows: ICompanyGroup[]; loading: boolean; error: string | null; + activeCompanyGroup?: ICompanyGroup | null; } diff --git a/src/store/store.ts b/src/store/store.ts index 1451d6d..17a4daf 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,5 +1,6 @@ import { configureStore } from '@reduxjs/toolkit'; import { useDispatch, useSelector } from 'react-redux'; +import catalogueJurReducer from './slices/catalogueJurSlice/catalogueJurSlice'; import comGroupsReducer from './slices/companyGroupsSlice/companyGroupsSlice'; import consolidationsReducer from './slices/consolidations/consolidationsSlice'; @@ -7,6 +8,7 @@ export const store = configureStore({ reducer: { company_groups: comGroupsReducer, consolidations: consolidationsReducer, + catalogue_jur: catalogueJurReducer, }, }); diff --git a/src/types/api.ts b/src/types/api.ts index 83d2d5d..52e0e9a 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -384,6 +384,7 @@ export interface RelatedGroupMemberOut { } export interface RelatedGroupDetail extends RelatedGroupOut { members: RelatedGroupMemberOut[]; + crm_id: string; } export interface LegalEntityBrief { id: string; diff --git a/src/types/catalogueJur/catalogueJur.ts b/src/types/catalogueJur/catalogueJur.ts new file mode 100644 index 0000000..5d6e472 --- /dev/null +++ b/src/types/catalogueJur/catalogueJur.ts @@ -0,0 +1,64 @@ +export interface ICatalogueJur { + id: string; + inn: string; + kpp?: string; + crm_id?: string; + entity_type: 'ЮЛ' | 'ИП'; + name: string; + company_group_id?: string; + company_group_name: string | null; + regional_office_id?: string; + close_date: Date; + valid_from: Date; + created_at: Date; + updated_at: Date; + outside_group: boolean; + closed: boolean | string; + memberships?: MemberShips[]; + aliases?: JurAlliase[]; + regional_office: { + id: string; + name: string; + is_head_office: boolean; + }; +} + +export type KindType = 'gsk' | 'gsz'; + +export type MemberShips = { + related_group_id: string; + code: string; + kind: KindType; + name: string; + valid_from: Date; +}; + +export type JurAlliase = { + alias: string; + created_at: Date; + id: string; + normalized_alias: string; +}; + +export type MoveCatalogueJurProps = { + company_group_id: string; + effective_date: string; +}; +export type WithdrawCatalogueJurProps = { + event: 'CLOSE' | 'REMOVE_FROM_GROUP'; + effective_date: string; + comment: string; +}; + +export type CatalogueJurFormProps = { + inn?: string; + kpp?: string | null; + crm_id: string; + entity_type: 'ЮЛ' | 'ИП'; + name: string; + aliases?: string[] | { value: string; label: string }[]; + company_group_id?: string; + regional_office_id?: string | null; + gsk_id?: string | null; + gsz_id?: string | null; +}; diff --git a/src/types/companyGroups/companyGroups.ts b/src/types/companyGroups/companyGroups.ts index 6c90b1c..8909640 100644 --- a/src/types/companyGroups/companyGroups.ts +++ b/src/types/companyGroups/companyGroups.ts @@ -17,3 +17,9 @@ export const toCompanyGroupRow = (g: ICompanyGroupOut): ICompanyGroup => ({ ...g, base_regional_office_name: g.base_regional_office?.name ?? '—', }); + +export interface CreateCompanyGroupProps { + name: string; + crm_id: string; + base_regional_office_id: string | null; +} diff --git a/src/types/relatedGroup/relatedGroup.ts b/src/types/relatedGroup/relatedGroup.ts new file mode 100644 index 0000000..7472809 --- /dev/null +++ b/src/types/relatedGroup/relatedGroup.ts @@ -0,0 +1,9 @@ +export type RelatedGroupFormType = { + name: string; + created_date: string | null; + crm_id: string; + kind?: 'gsk' | 'gsz'; + company_group_id?: string; +}; + +export type CreateEntityKind = 'gk' | 'ul' | 'gsk' | 'gsz'; diff --git a/src/utils/labels.test.ts b/src/utils/labels.test.ts index feef141..c9799da 100644 --- a/src/utils/labels.test.ts +++ b/src/utils/labels.test.ts @@ -7,6 +7,7 @@ import { lineStatusLabel, matchKindLabel, periodLabel, + pluralRu, quarterLabel, severityLabel, } from './labels'; @@ -74,6 +75,28 @@ describe('подписи периода', () => { }); }); +describe('pluralRu', () => { + const forms = ['запись', 'записи', 'записей'] as const; + it('1, 21, 101 — первая форма', () => { + expect(pluralRu(1, forms)).toBe('запись'); + expect(pluralRu(21, forms)).toBe('запись'); + expect(pluralRu(101, forms)).toBe('запись'); + }); + it('2–4, 22–24 — вторая форма', () => { + expect(pluralRu(2, forms)).toBe('записи'); + expect(pluralRu(4, forms)).toBe('записи'); + expect(pluralRu(23, forms)).toBe('записи'); + }); + it('0, 5–20, 11–14, 111 — третья форма', () => { + expect(pluralRu(0, forms)).toBe('записей'); + expect(pluralRu(5, forms)).toBe('записей'); + expect(pluralRu(11, forms)).toBe('записей'); + expect(pluralRu(12, forms)).toBe('записей'); + expect(pluralRu(14, forms)).toBe('записей'); + expect(pluralRu(111, forms)).toBe('записей'); + }); +}); + describe('подписи формы v5.19', () => { it('статус строки', () => { expect(lineStatusLabel('ok')).toBe('Строка формы'); diff --git a/src/utils/labels.ts b/src/utils/labels.ts index b27aab6..232e5f7 100644 --- a/src/utils/labels.ts +++ b/src/utils/labels.ts @@ -71,6 +71,16 @@ const MATCH_KIND: Record = { export const counterpartyTypeLabel = (t: CounterpartyType): string => COUNTERPARTY_TYPE[t] ?? t; export const matchKindLabel = (k: MatchKind | null): string => (k ? (MATCH_KIND[k] ?? k) : 'не сопоставлен'); +/** Форма слова по числу: `forms` — [1, 2–4, 5–20] («запись», «записи», «записей»). */ +export function pluralRu(count: number, forms: readonly [string, string, string]): string { + const mod100 = Math.abs(count) % 100; + const mod10 = mod100 % 10; + if (mod100 >= 11 && mod100 <= 14) return forms[2]; + if (mod10 === 1) return forms[0]; + if (mod10 >= 2 && mod10 <= 4) return forms[1]; + return forms[2]; +} + const LINE_STATUS: Record = { ok: 'Строка формы', unreadable: 'Код не распознан', diff --git a/src/utils/useNotification.tsx b/src/utils/useNotification.tsx new file mode 100644 index 0000000..c2444e3 --- /dev/null +++ b/src/utils/useNotification.tsx @@ -0,0 +1,24 @@ +import { App } from 'antd'; +import type { ReactNode } from 'react'; + +type NotifyArgs = { + title?: ReactNode; + description?: ReactNode; + placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; + duration?: number; +}; + +export function useNotificationApi() { + const { notification } = App.useApp(); + + return { + error: ({ title = 'Ошибка', description, placement = 'bottomRight', duration }: NotifyArgs) => + notification.error({ title: title, description, placement, duration }), + success: ({ title, description, placement = 'bottomRight', duration }: NotifyArgs) => + notification.success({ title: title, description, placement, duration }), + info: ({ title, description, placement = 'bottomRight', duration }: NotifyArgs) => + notification.info({ title: title, description, placement, duration }), + warning: ({ title, description, placement = 'bottomRight', duration }: NotifyArgs) => + notification.warning({ title: title, description, placement, duration }), + }; +} diff --git a/src/widgets/Admin/UsersTable.tsx b/src/widgets/Admin/UsersTable.tsx index 01a4464..e81ef42 100644 --- a/src/widgets/Admin/UsersTable.tsx +++ b/src/widgets/Admin/UsersTable.tsx @@ -12,7 +12,7 @@ const USERS_LIMIT = 200; // Загрузчики — функции уровня модуля: они не меняются между отрисовками (требование useAsyncData) const loadUsers = () => api.get>(`/admin/users?limit=${USERS_LIMIT}`); const loadRoles = () => api.get('/admin/roles'); -const loadOffices = () => api.get('/directories/regional-offices'); +export const loadOffices = () => api.get('/directories/regional-offices'); /** * Таблица пользователей: роли меняются прямо в строке (`PUT /admin/users/{id}/roles`), активность — переключателем diff --git a/src/widgets/CataloguGroupTabs/CataloguGroupTabs.css b/src/widgets/CataloguGroupTabs/CataloguGroupTabs.css new file mode 100644 index 0000000..6c551ff --- /dev/null +++ b/src/widgets/CataloguGroupTabs/CataloguGroupTabs.css @@ -0,0 +1,9 @@ +.ant-tabs-tab-active { + background-color: #ecfdf3 !important; +} + +.ant-tabs-tab { + padding: 8px 12px !important; + margin: 10px 0 0 0 !important; + border-radius: 8px !important; +} diff --git a/src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx b/src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx new file mode 100644 index 0000000..636ea7d --- /dev/null +++ b/src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx @@ -0,0 +1,33 @@ +import { Tabs } from 'antd'; +import { useAppSelector } from '../../store/store'; +import type { RelatedGroupOut } from '../../types/api'; +import './CataloguGroupTabs.css'; +interface CataloguGroupTabsI { + activeTab: CatalogueGroupTabKey; + setActiveTab: (val: CatalogueGroupTabKey) => void; + gskData: RelatedGroupOut[]; + gszData: RelatedGroupOut[]; +} + +export type CatalogueGroupTabKey = 'legal_entities' | 'gsk' | 'gsz'; + +export const CataloguGroupTabs = ({ activeTab, setActiveTab, gskData, gszData }: CataloguGroupTabsI) => { + const activeCompanyGroup = useAppSelector((state) => state.company_groups.activeCompanyGroup); + + return ( + setActiveTab(key as CatalogueGroupTabKey)} + tabBarStyle={{ margin: 0, marginBottom: 0 }} + style={{ marginBottom: 0 }} + items={[ + { + key: 'legal_entities', + label: `ЮЛ ${activeCompanyGroup?.legal_entities_count ?? 0}`, + }, + { key: 'gsk', label: `ГСК ${gskData?.length ?? 0}` }, + { key: 'gsz', label: `ГСЗ ${gszData?.length ?? 0}` }, + ]} + /> + ); +}; diff --git a/src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx b/src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx new file mode 100644 index 0000000..ff9ab01 --- /dev/null +++ b/src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx @@ -0,0 +1,33 @@ +import { Space } from 'antd'; +import Text from 'antd/es/typography/Text'; +import Title from 'antd/es/typography/Title'; +import { useAppSelector } from '../../store/store'; +import { pluralRu } from '../../utils/labels'; + +export const CompanyGroupHeader = () => { + const activeCompanyGroup = useAppSelector((state) => state.company_groups.activeCompanyGroup); + const count = activeCompanyGroup?.legal_entities_count ?? 0; + + const organizationWord = pluralRu(count, ['организация', 'организации', 'организаций']); + + return ( +
+ + {activeCompanyGroup?.name ?? ''} + + + + {activeCompanyGroup?.base_regional_office_name ?? ''} + + + {count ?? 0} {organizationWord} + + +
+ ); +}; diff --git a/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx b/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx new file mode 100644 index 0000000..bd865e0 --- /dev/null +++ b/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx @@ -0,0 +1,286 @@ +import { Col, Form, Input, Modal, Row, Select } from 'antd'; +import type React from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { ApiError } from '../../api/client'; +import { fetchOneCatalogueJur } from '../../services/api/directories/catalogue_jur'; +import { fetchRelatedGroups } from '../../services/api/directories/company_groups'; +import { useAppSelector } from '../../store/store'; +import type { RelatedGroupOut } from '../../types/api'; +import type { CatalogueJurFormProps } from '../../types/catalogueJur/catalogueJur'; +import { useNotificationApi } from '../../utils/useNotification'; +import { loadOffices } from '../Admin/UsersTable'; + +type Props = { + open: boolean; + onCancel: () => void; + onOk: (values: CatalogueJurFormProps) => void | Promise; + jurId?: string; +}; + +export const CatalogueJurForm: React.FC = ({ open, onCancel, onOk, jurId }) => { + const [form] = Form.useForm(); + const notification = useNotificationApi(); + const [initialDataLoading, setInitialDataLoading] = useState(false); + const [gskData, setGskData] = useState([]); + const [gszData, setGszData] = useState([]); + const [loadingRelatedGroupsOptions, setLoadingRelatedGroupsOptions] = useState(false); + const [loadingOfficesOptions, setLoadingOfficesOptions] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + const [officesOptions, setOfficesOptions] = useState<{ value: string; label: string }[]>([]); + + const companyGrops = useAppSelector((state) => state.company_groups.raw_rows); + const companyGropsLoading = useAppSelector((state) => state.company_groups.loading); + + const type = Form.useWatch('entity_type', form); + const companyGroupId = Form.useWatch('company_group_id', form); + + async function getInitialData() { + if (jurId) { + setInitialDataLoading(true); + try { + const data = await fetchOneCatalogueJur(jurId); + form.setFieldsValue({ + name: data?.name, + aliases: data?.aliases?.map((aliase) => aliase.alias), + crm_id: data?.crm_id, + entity_type: data?.entity_type, + inn: data.inn, + kpp: data?.kpp, + regional_office_id: data?.regional_office_id, + company_group_id: data?.company_group_id, + gsk_id: data.memberships?.find((elem) => elem.kind === 'gsk')?.related_group_id, + gsz_id: data.memberships?.find((elem) => elem.kind === 'gsz')?.related_group_id, + }); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ description: e.message }); + } + } finally { + setInitialDataLoading(false); + } + } + } + + async function getOfficesOptionsHandler() { + setLoadingOfficesOptions(true); + try { + const res = await loadOffices(); + const options = res.map((elem) => elem && { value: elem.id, label: elem.name }); + setOfficesOptions(options); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ description: e.message }); + } + } finally { + setLoadingOfficesOptions(false); + } + } + + async function getRelatedGroupsHandler() { + if (!companyGroupId) return; + + setLoadingRelatedGroupsOptions(true); + try { + const res = await fetchRelatedGroups(companyGroupId); + 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 { + setLoadingRelatedGroupsOptions(false); + } + } + + const handleOk = async () => { + setConfirmLoading(true); + try { + const values = await form.validateFields(); + await onOk({ + ...values, + kpp: type === 'ИП' ? null : values.kpp?.toString(), + inn: jurId ? undefined : values?.inn?.toString(), + company_group_id: jurId ? undefined : values.company_group_id, + gsz_id: values.gsz_id ? values?.gsz_id : null, + gsk_id: values.gsk_id ? values?.gsk_id : null, + regional_office_id: values.regional_office_id ? values?.regional_office_id : null, + }); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } finally { + setConfirmLoading(false); + } + }; + + const handleCancel = () => { + form.resetFields(); + onCancel(); + }; + + const companyGroupOptions = useMemo(() => { + return companyGrops?.map((company) => company && { value: company.id, label: company.name }); + }, [companyGrops]); + + useEffect(() => { + getInitialData(); + }, [jurId, open]); + + useEffect(() => { + getOfficesOptionsHandler(); + }, []); + + useEffect(() => { + form.setFieldsValue({ + gsk_id: undefined, + gsz_id: undefined, + }); + getRelatedGroupsHandler(); + }, [companyGroupId]); + + return ( + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ value: g.id, label: g.name }))} + showSearch + optionFilterProp='label' + notFoundContent={loadingRelatedGroupsOptions ? 'Загрузка' : 'Нет вариантов'} + /> + + + + + } + placeholder='Поиск по ГК, CRM ID или РФ' + value={rawQuery} + onChange={(e) => setRawQuery(e.target.value)} + style={{ maxWidth: 320 }} + /> + ) : ( + tabsComponent + )} + + + {hasActiveFilters && ( + + )} + dispatch(setIsGettingArchive(e.target.checked))}> + Архив + + + + + {relatedGroupFilter?.id && !outside_group && ( +
+
+ + Фильтр по: + {relatedGroupFilter.code} + + — {filterdData?.length}   + {pluralRu(filterdData?.length ?? 0, ['запись', 'записи', 'записей'])} + +
+ + +
+ )} + + {moveCatalogueFormOpen && ( + { + closeMoveCatalogueJurHandler(); + }} + onOk={async (values) => { + await moveCatalogueJurHandler(values); + }} + /> + )} + {withdrawCatalogueFormOpen && ( + { + closeWithdrawCatalogueJurHandler(); + }} + onOk={async (values) => { + await withdrawCatalogueJurHandler(values); + }} + /> + )} + {editCatalogueFormOpen && ( + { + closeEditCatalogueJurHandler(); + }} + onOk={async (values) => { + await editJurHandler(values); + }} + /> + )} + + ); +} diff --git a/src/widgets/CatalogueJurTable/config.tsx b/src/widgets/CatalogueJurTable/config.tsx new file mode 100644 index 0000000..48e3dc8 --- /dev/null +++ b/src/widgets/CatalogueJurTable/config.tsx @@ -0,0 +1,51 @@ +import type { TableConfig } from '../../components/GenericTable'; +import { TableTag } from '../../components/TableTag/TableTag'; +import { TAG_PRESETS } from '../../components/TableTag/presets'; +import type { ICatalogueJur } from '../../types/catalogueJur/catalogueJur'; + +export const catalogueJurConfig: TableConfig = { + rowKey: 'id', + searchableKeys: ['closed', 'inn', 'kpp', 'name', 'entity_type'], + columns: [ + { + key: 'closed', + title: 'Статус', + features: { sort: true }, + render: (_: unknown, row) => { + const preset = !row.closed ? TAG_PRESETS.green : TAG_PRESETS.gray; + const text = !row.closed ? 'Активно' : 'Не активно'; + + return ; + }, + }, + { + key: 'inn', + title: 'ИНН', + features: { search: true }, + }, + { + key: 'kpp', + title: 'КПП', + features: { search: true }, + }, + { + key: 'name', + title: 'Организация', + features: { search: true, multiselect: true, sort: true }, + }, + { + key: 'entity_type', + title: 'Тип', + features: { multiselect: true, sort: true }, + render: (val) => { + const preset = val === 'ЮЛ' ? TAG_PRESETS.orange : TAG_PRESETS.purple; + return ; + }, + }, + { + key: 'regional_office', + title: 'Основной РФ', + render: (val) => (val as ICatalogueJur['regional_office'] | null)?.name, + }, + ], +}; diff --git a/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx b/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx new file mode 100644 index 0000000..7f6653f --- /dev/null +++ b/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx @@ -0,0 +1,109 @@ +import { Col, Form, Input, Modal, Row, Select } from 'antd'; +import type React from 'react'; +import { useEffect, useState } from 'react'; +import { ApiError } from '../../api/client'; +import { useMe } from '../../auth/useMe'; +import { useAppSelector } from '../../store/store'; +import type { CreateCompanyGroupProps } from '../../types/companyGroups/companyGroups'; +import { useNotificationApi } from '../../utils/useNotification'; +import { loadOffices } from '../Admin/UsersTable'; + +type Props = { + open: boolean; + onCancel: () => void; + onOk: (values: CreateCompanyGroupProps) => void | Promise; +}; + +export const CompanyGroupForm: React.FC = ({ open, onCancel, onOk }) => { + const [form] = Form.useForm(); + const { me } = useMe(); + const notification = useNotificationApi(); + + const activeCompanyGroup = useAppSelector((state) => state.company_groups.activeCompanyGroup); + const [officesOptions, setOfficesOptions] = useState<{ value: string; label: string }[]>([]); + const [loadingOptions, setLoadingOptions] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + + async function getOfficesOptionsHandler() { + setLoadingOptions(true); + try { + const res = await loadOffices(); + const options = res.map((elem) => elem && { value: elem.id, label: elem.name }); + setOfficesOptions(options); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ description: e.message }); + } + } finally { + setLoadingOptions(false); + } + } + + const handleOk = async () => { + setConfirmLoading(true); + try { + const values = await form.validateFields(); + await onOk({ + ...values, + base_regional_office_id: values?.base_regional_office_id ? values?.base_regional_office_id : (me?.regional_office.id as string), + }); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } finally { + setConfirmLoading(false); + } + }; + + const handleCancel = () => { + form.resetFields(); + onCancel(); + }; + + useEffect(() => { + getOfficesOptionsHandler(); + }, [activeCompanyGroup]); + + return ( + + + + + + + + + + + + + + + } + placeholder='Поиск по ГК или CRM ID' + value={query} + onChange={(e) => setQuery(e.target.value)} + /> + + +
+ {filtered.length === 0 ? ( + + ) : ( +
+
+ { + const active = item.id === activeCompanyGroup?.id; + return ( + dispatch(setActiveCompanyGroup(item))} + style={{ + cursor: 'pointer', + borderRadius: 8, + padding: '10px 12px', + marginBottom: 4, + background: active ? '#e6f4ea' : 'transparent', + boxShadow: active ? 'inset 0 0 0 1px #2ea64b3d, inset 3px 0 0 #258141' : 'inset 0 0 0 1px #0f172a14', + transition: 'background 0.15s', + }} + onMouseEnter={(e) => { + if (!active) { + (e.currentTarget as HTMLElement).style.background = '#fafafa'; + } + }} + onMouseLeave={(e) => { + if (!active) { + (e.currentTarget as HTMLElement).style.background = 'transparent'; + } + }}> +
+ + +
+ + {item.name} + + + CRM ID: {item.crm_id ?? '-'} + +
+ + + {item.legal_entities_count} + +
+
+ ); + }} + /> +
+
+ )} +
+ + ); +}; diff --git a/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.css b/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.css new file mode 100644 index 0000000..727dff7 --- /dev/null +++ b/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.css @@ -0,0 +1,67 @@ +/* Обёртка списка опций */ +.options { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Карточка */ +.optionCard { + display: flex; + align-items: center; + gap: 16px; + padding: 12px; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 12px; + cursor: pointer; + text-align: left; + width: 100%; + font: inherit; + transition: + border-color 0.15s, + background 0.15s; +} + +.optionCard:hover { + background: #f6fbf7; + border-color: #b7e0c2; +} + +.optionCard:focus-visible { + outline: 2px solid #2e7d32; + outline-offset: 2px; +} + +/* Иконка в круге */ +.optionIcon { + width: 44px; + height: 44px; + border-radius: 10px; + background: #e6f4ea; + color: #2e7d32; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} + +/* Текстовый блок */ +.optionText { + min-width: 0; +} + +.optionTitle { + font-size: 16px; + font-weight: 600; + color: #1f1f1f; + line-height: 22px; +} + +.optionDescription { + font-size: 13px; + color: #8c8c8c; + line-height: 18px; + margin-top: 2px; +} diff --git a/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.tsx b/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.tsx new file mode 100644 index 0000000..ea012bf --- /dev/null +++ b/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.tsx @@ -0,0 +1,76 @@ +import { ApartmentOutlined, BankOutlined, DeploymentUnitOutlined, TeamOutlined } from '@ant-design/icons'; +import { Modal } from 'antd'; +import type React from 'react'; +import type { CreateEntityKind } from '../../types/relatedGroup/relatedGroup'; +import './CreateEntityPickerModal.css'; +type Option = { + key: CreateEntityKind; + title: string; + description: string; + icon: React.ReactNode; +}; + +const OPTIONS: Option[] = [ + { + key: 'gk', + title: 'ГК', + description: 'Группа компаний', + icon: , + }, + { + key: 'ul', + title: 'ЮЛ', + description: 'Юридическое лицо', + icon: , + }, + { + key: 'gsk', + title: 'ГСК', + description: 'Группа связанных компаний', + icon: , + }, + { + key: 'gsz', + title: 'ГСЗ', + description: 'Группа связанных заёмщиков', + icon: , + }, +]; + +type Props = { + open: boolean; + onCancel: () => void; + onSelect: (kind: CreateEntityKind) => void; +}; + +export const CreateEntityPickerModal: React.FC = ({ open, onCancel, onSelect }) => { + return ( + +
+ {OPTIONS.map((opt) => ( + + ))} +
+
+ ); +}; diff --git a/src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx b/src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx new file mode 100644 index 0000000..96ebc5a --- /dev/null +++ b/src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx @@ -0,0 +1,26 @@ +import { Modal } from 'antd'; + +type Props = { + open: boolean; + onCancel: () => void; + onOk: () => void | Promise; + loading?: boolean; + kind: 'gsk' | 'gsz'; +}; + +export const DeleteRelatedGroupModal: React.FC = ({ open, onCancel, onOk, loading, kind }) => { + return ( + +

Группа связанных {kind === 'gsk' ? 'компаний' : 'заёмщиков'} будет удалена.

+
+ ); +}; diff --git a/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx b/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx new file mode 100644 index 0000000..61ed5c5 --- /dev/null +++ b/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx @@ -0,0 +1,132 @@ +import { DatePicker, Form, Input, Modal, Select } from 'antd'; +import FormItem from 'antd/es/form/FormItem'; +import dayjs from 'dayjs'; +import type React from 'react'; +import { useEffect, useState } from 'react'; +import { ApiError } from '../../api/client'; +import { fetchOneCatalogueJur } from '../../services/api/directories/catalogue_jur'; +import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks'; +import { useAppDispatch, useAppSelector } from '../../store/store'; +import type { MoveCatalogueJurProps } from '../../types/catalogueJur/catalogueJur'; +import { useNotificationApi } from '../../utils/useNotification'; + +type FormValues = { + name: string; + company_group_name: string; + company_group_id: string; + effective_date: string; + old_company_group_id: string; +}; + +type Props = { + open: boolean; + onCancel: () => void; + onOk: (values: MoveCatalogueJurProps) => void | Promise; + jurId: string; +}; + +export const MoveCatalogueJurForm: React.FC = ({ open, onCancel, onOk, jurId }) => { + const [form] = Form.useForm(); + const dispatch = useAppDispatch(); + const notification = useNotificationApi(); + + const { raw_rows, loading } = useAppSelector((state) => state.company_groups); + const [initialDataLoading, setInitialDataLoading] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + + async function getInitialData() { + setInitialDataLoading(true); + try { + const data = await fetchOneCatalogueJur(jurId); + form.setFieldsValue({ + name: data?.name, + company_group_name: data?.company_group_name ?? '', + old_company_group_id: data?.company_group_id ?? '', + company_group_id: undefined, + effective_date: '', + }); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ description: e.message }); + } + } finally { + setInitialDataLoading(false); + } + } + + const handleOk = async () => { + setConfirmLoading(true); + try { + const { effective_date, company_group_id } = await form.validateFields(); + await onOk({ effective_date, company_group_id }); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } finally { + setConfirmLoading(false); + } + }; + + const handleCancel = () => { + form.resetFields(); + onCancel(); + }; + useEffect(() => { + if (open && raw_rows.length === 0 && !loading) { + dispatch(getCompanyGroupsThunk()); + } + }, [open, raw_rows.length, loading, dispatch]); + + useEffect(() => { + getInitialData(); + }, [open]); + + return ( + + + + + + + + + + + + + + + + + + +