From 25d2211ae68c969bfb1921108ac4ee6cc4fcda60 Mon Sep 17 00:00:00 2001 From: Grog Date: Mon, 19 Oct 2026 09:33:27 +0300 Subject: [PATCH] fixed bugs --- src/components/TableTag/presets.tsx | 5 + src/hooks/useVirtualList.tsx | 51 ++++ src/layouts/CatalogueGroups.tsx | 23 +- src/services/api/directories/catalogue_jur.ts | 3 +- .../api/directories/company_groups.ts | 2 +- src/types/catalogueJur/catalogueJur.ts | 8 +- .../CatalogueJurForm/CatalogueJurForm.tsx | 90 ++++--- .../CatalogueJurTable/CatalogueJurTable.tsx | 6 +- src/widgets/CatalogueJurTable/config.tsx | 6 +- .../CompanyGroupForm/CompanyGroupForm.tsx | 21 +- .../CompanyGroupsList/CompanyGroupsList.tsx | 221 +++++++++--------- .../DeleteRelatedGroupModal.tsx | 5 +- .../MoveCatalogueJurForm.tsx | 25 +- .../RelatedGroupForm/RelatedGroupForm.tsx | 11 +- .../RelatedGroupTable/RelatedGroupTable.tsx | 1 + .../WithdrawCatalogueJurForm.tsx | 25 +- 16 files changed, 308 insertions(+), 195 deletions(-) create mode 100644 src/hooks/useVirtualList.tsx diff --git a/src/components/TableTag/presets.tsx b/src/components/TableTag/presets.tsx index f35feab..3647e0f 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..fdd04b9 --- /dev/null +++ b/src/hooks/useVirtualList.tsx @@ -0,0 +1,51 @@ +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, + }; +} \ No newline at end of file diff --git a/src/layouts/CatalogueGroups.tsx b/src/layouts/CatalogueGroups.tsx index 04a6ed0..9509c4a 100644 --- a/src/layouts/CatalogueGroups.tsx +++ b/src/layouts/CatalogueGroups.tsx @@ -95,15 +95,24 @@ export function CatalogueGroup() { } async function getRelatedGroupsHandler() { - setRelatedGroupLoading(true); if (activeCompanyGroup?.id) { - 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(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); + } } - setRelatedGroupLoading(false); } useEffect(() => { diff --git a/src/services/api/directories/catalogue_jur.ts b/src/services/api/directories/catalogue_jur.ts index 4a7c38c..c84de06 100644 --- a/src/services/api/directories/catalogue_jur.ts +++ b/src/services/api/directories/catalogue_jur.ts @@ -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>('/directories/legal-entities', params); + const res = await api.get>('/directories/legal-entities', {limit:LE_PAGE, offset:0, ...params}); return res; } diff --git a/src/services/api/directories/company_groups.ts b/src/services/api/directories/company_groups.ts index 077609d..9ca3f26 100644 --- a/src/services/api/directories/company_groups.ts +++ b/src/services/api/directories/company_groups.ts @@ -3,7 +3,7 @@ import type { LegalEntityBrief, Page, RelatedGroupDetail, RelatedGroupOut } from 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 { diff --git a/src/types/catalogueJur/catalogueJur.ts b/src/types/catalogueJur/catalogueJur.ts index 582e75e..5d6e472 100644 --- a/src/types/catalogueJur/catalogueJur.ts +++ b/src/types/catalogueJur/catalogueJur.ts @@ -52,13 +52,13 @@ export type WithdrawCatalogueJurProps = { export type CatalogueJurFormProps = { inn?: string; - kpp?: 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; - gsk_id?: string; - gsz_id?: string; + regional_office_id?: string | null; + gsk_id?: string | null; + gsz_id?: string | null; }; diff --git a/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx b/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx index b97f8b6..2392b8b 100644 --- a/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx +++ b/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx @@ -1,4 +1,4 @@ -import { Col, Form, Input, InputNumber, Modal, Row, Select } from 'antd'; +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'; @@ -37,40 +37,62 @@ export const CatalogueJurForm: React.FC = ({ open, onCancel, onOk, jurId async function getInitialData() { if (jurId) { setInitialDataLoading(true); - 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, - }); - - setInitialDataLoading(false); + 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 }); + } else { + console.error(e); + } + } finally { + setInitialDataLoading(false); + } } } async function getOfficesOptionsHandler() { setLoadingOfficesOptions(true); - const res = await loadOffices(); - const options = res.map((elem) => elem && { value: elem.id, label: elem.name }); - setOfficesOptions(options); - setLoadingOfficesOptions(false); + 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) { - setLoadingRelatedGroupsOptions(true); + 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); } } @@ -81,15 +103,20 @@ export const CatalogueJurForm: React.FC = ({ open, onCancel, onOk, jurId const values = await form.validateFields(); await onOk({ ...values, - kpp: type === 'ИП' ? undefined : values.kpp?.toString(), + 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, }); + } else if (e instanceof Error && e.name !== 'ValidationError') { + console.error(e); } } finally { setConfirmLoading(false); @@ -112,7 +139,12 @@ export const CatalogueJurForm: React.FC = ({ open, onCancel, onOk, jurId useEffect(() => { getOfficesOptionsHandler(); }, []); + useEffect(() => { + form.setFieldsValue({ + gsk_id: undefined, + gsz_id: undefined, + }); getRelatedGroupsHandler(); }, [companyGroupId]); @@ -122,7 +154,7 @@ export const CatalogueJurForm: React.FC = ({ open, onCancel, onOk, jurId open={open} onCancel={handleCancel} onOk={handleOk} - okText={jurId ? 'Сохраить' : 'Добавить'} + okText={jurId ? 'Сохранить' : 'Добавить'} cancelText='Отмена' destroyOnHidden width={720} @@ -145,7 +177,7 @@ export const CatalogueJurForm: React.FC = ({ open, onCancel, onOk, jurId { pattern: /^\d{10}$/, message: '10 цифр' }, ] }> - = ({ open, onCancel, onOk, jurId - - + + - - + + diff --git a/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx b/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx index 26f3dd6..9306b33 100644 --- a/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx +++ b/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx @@ -54,7 +54,7 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur const [jurId, setJurId] = useState(''); const filterdData = useMemo(() => { - if (relatedGroupFilter?.id) { + if (relatedGroupFilter?.id && !outside_group) { return rawRows?.filter((row) => { return row?.memberships?.some((member) => member.related_group_id === relatedGroupFilter.id); }); @@ -243,7 +243,7 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur - {relatedGroupFilter?.id && ( + {(relatedGroupFilter?.id && !outside_group) && (
Фильтр по: {relatedGroupFilter.code} - — {filterdData?.length} + — {filterdData?.length}   {pluralRu(filterdData?.length ?? 0, ['запись', 'записи', 'записей'])}
diff --git a/src/widgets/CatalogueJurTable/config.tsx b/src/widgets/CatalogueJurTable/config.tsx index d0b1951..48e3dc8 100644 --- a/src/widgets/CatalogueJurTable/config.tsx +++ b/src/widgets/CatalogueJurTable/config.tsx @@ -12,8 +12,10 @@ export const catalogueJurConfig: TableConfig = { title: 'Статус', features: { sort: true }, render: (_: unknown, row) => { - // #TODO поменять логику если запись не активна - return ; + const preset = !row.closed ? TAG_PRESETS.green : TAG_PRESETS.gray; + const text = !row.closed ? 'Активно' : 'Не активно'; + + return ; }, }, { diff --git a/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx b/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx index 4c12184..c970080 100644 --- a/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx +++ b/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx @@ -26,10 +26,17 @@ export const CompanyGroupForm: React.FC = ({ open, onCancel, onOk }) => { async function getOfficesOptionsHandler() { setLoadingOptions(true); - const res = await loadOffices(); - const options = res.map((elem) => elem && { value: elem.id, label: elem.name }); - setOfficesOptions(options); - setLoadingOptions(false); + 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 () => { @@ -45,6 +52,8 @@ export const CompanyGroupForm: React.FC = ({ open, onCancel, onOk }) => { notification.error({ description: e.message, }); + } else if (e instanceof Error && e.name !== 'ValidationError') { + console.error(e); } } finally { setConfirmLoading(false); @@ -78,8 +87,8 @@ export const CompanyGroupForm: React.FC = ({ open, onCancel, onOk }) => { - - + + diff --git a/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx b/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx index ed60e06..bfea78e 100644 --- a/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx +++ b/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx @@ -1,10 +1,11 @@ -import { FolderOpenOutlined, SearchOutlined } from '@ant-design/icons'; -import { Empty, Input, Listy, Spin, Tag, Typography } from 'antd'; import { useEffect, useMemo, useState } from 'react'; -import { setActiveCompanyGroup } from '../../store/slices/companyGroupsSlice/companyGroupsSlice'; -import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks'; +import { Input, List, Spin, Empty, Typography, Tag } from 'antd'; +import { FolderOpenOutlined, SearchOutlined } from '@ant-design/icons'; import { useAppDispatch, useAppSelector } from '../../store/store'; -import './CompanyGroupsList.css'; +import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks'; +import { setActiveCompanyGroup } from '../../store/slices/companyGroupsSlice/companyGroupsSlice'; +import { useVirtualList } from '../../hooks/useVirtualList'; + const { Text } = Typography; export const CompanyGroupsList = () => { @@ -22,10 +23,22 @@ export const CompanyGroupsList = () => { return rawRows.filter((g) => g.name.toLowerCase().includes(q) || g.crm_id?.toLowerCase().includes(q)); }, [rawRows, query]); + const { ref, onScroll, visibleItems, totalHeight, offsetY } = useVirtualList(filtered, { + itemHeight: 56, + containerHeight: 900, + overscan: 5, + }); + useEffect(() => { dispatch(getCompanyGroupsThunk()); }, [dispatch]); + useEffect(() => { + if (!activeCompanyGroup && rawRows?.length) { + dispatch(setActiveCompanyGroup(rawRows?.[0])); + } + }, [rawRows, activeCompanyGroup, dispatch]); + return (
{ borderRight: '1px solid #f0f0f0', borderRadius: 12, }}> -
+
} @@ -46,124 +59,100 @@ export const CompanyGroupsList = () => { />
-
- {loading && ( +
+ {loading ? (
- )} - {!loading && filtered.length === 0 && ( + ) : filtered.length === 0 ? ( - )} - {!loading && filtered.length > 0 && ( - { - const active = item.id === activeCompanyGroup?.id; - return ( - - ); - }} - /> +
+ + +
+ + {item.name} + + + CRM ID: {item.crm_id ?? '-'} + +
+ + + {item.legal_entities_count} + +
+ + ); + }} + /> +
+
)}
diff --git a/src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx b/src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx index 49f00ef..96ebc5a 100644 --- a/src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx +++ b/src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx @@ -5,9 +5,10 @@ type Props = { onCancel: () => void; onOk: () => void | Promise; loading?: boolean; + kind: 'gsk' | 'gsz'; }; -export const DeleteRelatedGroupModal: React.FC = ({ open, onCancel, onOk, loading }) => { +export const DeleteRelatedGroupModal: React.FC = ({ open, onCancel, onOk, loading, kind }) => { return ( = ({ open, onCancel, onOk, okButtonProps={{ danger: true }} confirmLoading={loading} centered> -

Группа связанных компаний будет удалена.

+

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

); }; diff --git a/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx b/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx index 1ae81ae..61ed5c5 100644 --- a/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx +++ b/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx @@ -36,15 +36,22 @@ export const MoveCatalogueJurForm: React.FC = ({ open, onCancel, onOk, ju async function getInitialData() { setInitialDataLoading(true); - 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: '', - }); - setInitialDataLoading(false); + 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 () => { diff --git a/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx b/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx index 792b7fe..60d0f58 100644 --- a/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx +++ b/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx @@ -36,7 +36,7 @@ export const RelatedGroupForm: React.FC = ({ open, onCancel, onOk, relate form.setFieldsValue({ crm_id: data?.crm_id, name: data?.name, - created_date: data?.created_date, + created_date: data?.created_date ?? dayjs().format('YYYY-MM-DD'), company_group_id: data?.company_group_id, }); } catch (e) { @@ -53,7 +53,7 @@ export const RelatedGroupForm: React.FC = ({ open, onCancel, onOk, relate try { const { created_date, name, crm_id, company_group_id } = await form.validateFields(); await onOk({ - created_date, + created_date: created_date ?? dayjs().format('YYYY-MM-DD'), name, crm_id, company_group_id: relatedGroupId ? undefined : company_group_id, @@ -85,7 +85,7 @@ export const RelatedGroupForm: React.FC = ({ open, onCancel, onOk, relate return ( = ({ open, onCancel, onOk, relate loading={initialDataLoading} confirmLoading={confirmLoading}>
- - + + @@ -115,7 +115,6 @@ export const RelatedGroupForm: React.FC = ({ open, onCancel, onOk, relate ({ value: value ? dayjs(value) : undefined, })} diff --git a/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx b/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx index b2e6737..05ed702 100644 --- a/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx +++ b/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx @@ -180,6 +180,7 @@ export function RelatedGroupTable({ tabsComponent, data, loading, getData, activ loading={deletingLoading} onCancel={() => closeDeleteRelatedGroupHandler()} onOk={deleteRelatedGroupHandler} + kind={activeTab} /> )} diff --git a/src/widgets/WithdrawCatalogueJurForm/WithdrawCatalogueJurForm.tsx b/src/widgets/WithdrawCatalogueJurForm/WithdrawCatalogueJurForm.tsx index f71d1a1..966f02d 100644 --- a/src/widgets/WithdrawCatalogueJurForm/WithdrawCatalogueJurForm.tsx +++ b/src/widgets/WithdrawCatalogueJurForm/WithdrawCatalogueJurForm.tsx @@ -34,14 +34,21 @@ export const WithdrawCatalogueJurForm: React.FC = ({ open, onCancel, onOk async function getInitialData() { setInitialDataLoading(true); - const data = await fetchOneCatalogueJur(jurId); - form.setFieldsValue({ - name: data?.name, - event: undefined, - effective_date: '', - comment: '', - }); - setInitialDataLoading(false); + try { + const data = await fetchOneCatalogueJur(jurId); + form.setFieldsValue({ + name: data?.name, + event: undefined, + effective_date: '', + comment: '', + }); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ description: e.message }); + } + } finally { + setInitialDataLoading(false); + } } const handleOk = async () => { @@ -100,7 +107,7 @@ export const WithdrawCatalogueJurForm: React.FC = ({ open, onCancel, onOk -