From 71dae2173db3088e207903cccdcdbfa649f65526 Mon Sep 17 00:00:00 2001 From: Grog Date: Thu, 15 Oct 2026 16:11:02 +0300 Subject: [PATCH] updated company group and forms --- src/App.tsx | 108 +++++---- src/layouts/CatalogueGroups.tsx | 209 +++++++++++++++-- src/layouts/CatalogueJur.tsx | 27 ++- .../api/directories/company_groups.ts | 11 +- .../api/directories/related-groups.ts | 23 ++ .../catalogueJurSlice/catalogueJurSlice.ts | 7 +- src/store/slices/catalogueJurSlice/types.ts | 4 +- .../companyGroupsSlice/companyGroupsSlice.ts | 6 + src/types/api.ts | 1 + src/types/companyGroups/companyGroups.ts | 6 + src/types/relatedGroup/relatedGroup.ts | 9 + src/utils/useNotification.tsx | 24 ++ src/widgets/Admin/UsersTable.tsx | 2 +- .../CataloguGroupTabs/CataloguGroupTabs.css | 11 + .../CataloguGroupTabs/CataloguGroupTabs.tsx | 41 ++++ .../CatalogueGroupHeader.tsx | 40 ++++ .../CatalogueJurForm/CatalogueJurForm.tsx | 157 +++++++++---- .../CatalogueJurTable/CatalogueJurTable.tsx | 196 ++++++++++------ src/widgets/CatalogueJurTable/config.tsx | 26 +- .../CompanyGroupForm/CompanyGroupForm.tsx | 124 ++++++++++ .../CompanyGroupsList/CompanyGroupsList.css | 4 + .../CompanyGroupsList/CompanyGroupsList.tsx | 31 ++- .../CreateEntityPickerModal.css | 65 +++++ .../CreateEntityPickerModal.tsx | 91 +++++++ .../DeleteRelatedGroupModal.tsx | 33 +++ .../MoveCatalogueJurForm.tsx | 18 +- .../RelatedGroupForm/RelatedGroupForm.tsx | 158 +++++++++++++ .../RelatedGroupTable/RelatedGroupTable.tsx | 222 ++++++++++++++++++ src/widgets/RelatedGroupTable/config.tsx | 40 ++++ .../WithdrawCatalogueJurForm.tsx | 20 +- 30 files changed, 1492 insertions(+), 222 deletions(-) create mode 100644 src/services/api/directories/related-groups.ts create mode 100644 src/types/relatedGroup/relatedGroup.ts create mode 100644 src/utils/useNotification.tsx create mode 100644 src/widgets/CataloguGroupTabs/CataloguGroupTabs.css create mode 100644 src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx create mode 100644 src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx create mode 100644 src/widgets/CompanyGroupForm/CompanyGroupForm.tsx create mode 100644 src/widgets/CompanyGroupsList/CompanyGroupsList.css create mode 100644 src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.css create mode 100644 src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.tsx create mode 100644 src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx create mode 100644 src/widgets/RelatedGroupForm/RelatedGroupForm.tsx create mode 100644 src/widgets/RelatedGroupTable/RelatedGroupTable.tsx create mode 100644 src/widgets/RelatedGroupTable/config.tsx diff --git a/src/App.tsx b/src/App.tsx index 2adc581..3d5b6a9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,55 +1,67 @@ -import './App.css'; -import { Layout } from 'antd'; -import { Provider } from 'react-redux'; -import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import { AuthGate } from './auth/AuthGate'; -import { Config } from './conf/config'; +import "./App.css"; +import { Layout } from "antd"; +import { Provider } from "react-redux"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { AuthGate } from "./auth/AuthGate"; +import { Config } from "./conf/config"; import { - Admin, - AuditLogging, - CatalogueGroup, - CatalogueJur, - ConsolidatedOsvPage, - ConsolidationPage, - DocStorage, - DocumentPage, - GroupsLayout, - Main, - Mono, -} from './layouts/'; -import { store } from './store/store'; -import { Sidebar } from './widgets/Sidebar/Sidebar'; + Admin, + AuditLogging, + CatalogueGroup, + CatalogueJur, + ConsolidatedOsvPage, + ConsolidationPage, + DocStorage, + DocumentPage, + GroupsLayout, + Main, + Mono, +} from "./layouts/"; +import { store } from "./store/store"; +import { Sidebar } from "./widgets/Sidebar/Sidebar"; +import { App as AntApp } from "antd"; // Каркас: экраны по docs/context/10-user-journey.md добавляются срезами. function App() { - return ( - - - - - - - - } /> - } /> - - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - - - - - - - ); + return ( + + + + + + + + + } /> + } /> + + } /> + } /> + } + /> + } /> + + } + /> + } /> + } /> + } + /> + } /> + + + + + + + + ); } export default App; diff --git a/src/layouts/CatalogueGroups.tsx b/src/layouts/CatalogueGroups.tsx index 9a24e1e..1023ff1 100644 --- a/src/layouts/CatalogueGroups.tsx +++ b/src/layouts/CatalogueGroups.tsx @@ -2,31 +2,139 @@ import { Button, Space } from "antd"; import PageTitle from "../components/PageTitle"; import { CatalogueJurTable } from "../widgets/CatalogueJurTable/CatalogueJurTable"; import { PlusOutlined } from "@ant-design/icons"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useAppDispatch, useAppSelector } from "../store/store"; import { CatalogueJurForm } from "../widgets/CatalogueJurForm/CatalogueJurForm"; import { getCatalogueJurThunk } from "../store/slices/catalogueJurSlice/thunks"; import { CatalogueJurFormProps } from "../types/catalogueJur/catalogueJur"; import { createCatalogueJur } from "../services/api/directories/catalogue_jur"; import { CompanyGroupsList } from "../widgets/CompanyGroupsList/CompanyGroupsList"; +import { RelatedGroupOut } from "../types/api"; +import { + createCompanyGroup, + fetchRelatedGroups, +} from "../services/api/directories/company_groups"; +import { RelatedGroupTable } from "../widgets/RelatedGroupTable/RelatedGroupTable"; +import { CreateEntityPickerModal } from "../widgets/CreateEntityPickerModal/CreateEntityPickerModal"; +import { + CreateEntityKind, + RelatedGroupFormType, +} from "../types/relatedGroup/relatedGroup"; +import { createRelatedGroup } from "../services/api/directories/related-groups"; +import { RelatedGroupForm } from "../widgets/RelatedGroupForm/RelatedGroupForm"; +import { CompanyGroupForm } from "../widgets/CompanyGroupForm/CompanyGroupForm"; +import { CreateCompanyGroupProps } from "../types/companyGroups/companyGroups"; +import { getCompanyGroupsThunk } from "../store/slices/companyGroupsSlice/thunks"; +import { setActiveCompanyGroup } from "../store/slices/companyGroupsSlice/companyGroupsSlice"; +import { useNotificationApi } from "../utils/useNotification"; +import { ApiError } from "../api/client"; +import { CompanyGroupHeader } from "../widgets/CatalogueGroupHeader/CatalogueGroupHeader"; +import { + CatalogueGroupTabKey, + CataloguGroupTabs, +} from "../widgets/CataloguGroupTabs/CataloguGroupTabs"; export function CatalogueGroup() { const dispatch = useAppDispatch(); + const notification = useNotificationApi(); + const isGettingArchive = useAppSelector( (state) => state.catalogue_jur.isGettingArchive, ); - const [createFormOpen, setCreateFormOpen] = useState(false); + 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: true }), + getCatalogueJurThunk({ + closed: isGettingArchive, + outside_group: false, + company_group_id: activeCompanyGroup?.id, + }), ); - setCreateFormOpen(false); - } catch {} + 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() { + 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(false); + } + + useEffect(() => { + getRelatedGroupsHandler(); + }, [activeCompanyGroup]); + + useEffect(() => { + setActiveTab("legal_entities"); + }, [relatedGroupFilter]); + + useEffect(() => { + if (!activeCompanyGroup && rawRows?.length) { + dispatch(setActiveCompanyGroup(rawRows?.[0])); + } + }, [rawRows, activeCompanyGroup]); return (
} - onClick={() => setCreateFormOpen(true)} + onClick={() => setEntityCreateModalOpen(true)} > Создать ГК/ЮЛ @@ -59,21 +167,94 @@ export function CatalogueGroup() { }} >
- +
-
- +
+ + {activeTab === "legal_entities" && ( + setActiveTab(val)} + gskData={gskData} + gszData={gszData} + /> + } + /> + )} + {(activeTab === "gsk" || activeTab === "gsz") && ( + getRelatedGroupsHandler()} + tabsComponent={ + setActiveTab(val)} + gskData={gskData} + gszData={gszData} + /> + } + loading={relatedGroupLoading} + /> + )}
- {createFormOpen && ( + { + setEntityCreateModalOpen(false); + }} + onSelect={(kind) => { + setKindForm(kind); + setEntityCreateModalOpen(false); + }} + /> + {kindForm === "ul" && ( { - setCreateFormOpen(false); + setKindForm(null); }} - onOk={(values) => { - createJurHandler(values); + 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 eaa9468..7aa9170 100644 --- a/src/layouts/CatalogueJur.tsx +++ b/src/layouts/CatalogueJur.tsx @@ -8,23 +8,36 @@ import { CatalogueJurForm } from "../widgets/CatalogueJurForm/CatalogueJurForm"; import { getCatalogueJurThunk } from "../store/slices/catalogueJurSlice/thunks"; import { CatalogueJurFormProps } from "../types/catalogueJur/catalogueJur"; import { createCatalogueJur } from "../services/api/directories/catalogue_jur"; +import { ApiError } from "../api/client"; +import { useNotificationApi } from "../utils/useNotification"; export function CatalogueJur() { const dispatch = useAppDispatch(); + const notification = useNotificationApi(); + const isGettingArchive = useAppSelector( (state) => state.catalogue_jur.isGettingArchive, ); - + const [createFormOpen, setCreateFormOpen] = useState(false); - const outside_group = true + const outside_group = true; async function createJurHandler(values: CatalogueJurFormProps) { try { await createCatalogueJur(values); dispatch( - getCatalogueJurThunk({ closed: isGettingArchive, outside_group: outside_group }), + getCatalogueJurThunk({ + closed: isGettingArchive, + outside_group: outside_group, + }), ); setCreateFormOpen(false); - } catch {} + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } } return ( @@ -46,15 +59,15 @@ export function CatalogueJur() { Создать ЮЛ - + {createFormOpen && ( { setCreateFormOpen(false); }} - onOk={(values) => { - createJurHandler(values); + onOk={async(values) => { + await createJurHandler(values); }} /> )} diff --git a/src/services/api/directories/company_groups.ts b/src/services/api/directories/company_groups.ts index be04b11..42f1ab0 100644 --- a/src/services/api/directories/company_groups.ts +++ b/src/services/api/directories/company_groups.ts @@ -1,6 +1,6 @@ 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; @@ -36,3 +36,12 @@ export async function fetchAllLegalEntities(groupId: string): Promise api.get(`/directories/legal-entities/${id}`); + +export async 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..d6d51b5 --- /dev/null +++ b/src/services/api/directories/related-groups.ts @@ -0,0 +1,23 @@ +import { api } from "../../../api/client"; +import { RelatedGroupFormType } from "../../../types/relatedGroup/relatedGroup"; + +export async function editRelatedGroup( + groupId: string, + body: RelatedGroupFormType, +): Promise { + return api.patch( + `/directories/related-groups/${groupId}`, + body, + ); +} +export async 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 index 24bce55..135fd73 100644 --- a/src/store/slices/catalogueJurSlice/catalogueJurSlice.ts +++ b/src/store/slices/catalogueJurSlice/catalogueJurSlice.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { CatalogueJurSliceType } from "./types"; import { getCatalogueJurThunk } from "./thunks"; +import { RelatedGroupOut } from "../../../types/api"; const initialState: CatalogueJurSliceType = { raw_rows: [], @@ -8,6 +9,7 @@ const initialState: CatalogueJurSliceType = { loading: false, error: null, isGettingArchive: false, + relatedGroupFilter: null, }; const catalogueJurSlice = createSlice({ @@ -17,6 +19,9 @@ const catalogueJurSlice = createSlice({ setIsGettingArchive(state, action: PayloadAction) { state.isGettingArchive = action.payload; }, + setRelatedGroupFilter(state, action: PayloadAction) { + state.relatedGroupFilter = action.payload; + }, }, extraReducers: (builder) => { builder @@ -35,5 +40,5 @@ const catalogueJurSlice = createSlice({ }); }, }); -export const { setIsGettingArchive } = catalogueJurSlice.actions; +export const { setIsGettingArchive, setRelatedGroupFilter } = catalogueJurSlice.actions; export default catalogueJurSlice.reducer; diff --git a/src/store/slices/catalogueJurSlice/types.ts b/src/store/slices/catalogueJurSlice/types.ts index 2ff7f05..b8a4e55 100644 --- a/src/store/slices/catalogueJurSlice/types.ts +++ b/src/store/slices/catalogueJurSlice/types.ts @@ -1,3 +1,4 @@ +import { RelatedGroupOut } from "../../../types/api"; import { ICatalogueJur } from "../../../types/catalogueJur/catalogueJur"; export interface CatalogueJurSliceType{ @@ -5,7 +6,8 @@ export interface CatalogueJurSliceType{ raw_rows: ICatalogueJur[]; loading: boolean; error: string | null; - isGettingArchive:boolean + isGettingArchive:boolean; + relatedGroupFilter?:RelatedGroupOut | null } diff --git a/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts b/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts index 09692d7..33f0e9c 100644 --- a/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts +++ b/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts @@ -27,6 +27,12 @@ const comGroupsSlice = createSlice({ }) .addCase(getCompanyGroupsThunk.fulfilled, (state, action) => { state.raw_rows = action.payload; + if (state.activeCompanyGroup?.id) { + let newActiveCompanyGroup = action.payload.find( + (elem) => elem.id === state.activeCompanyGroup?.id, + ); + state.activeCompanyGroup = newActiveCompanyGroup; + } state.total_groups = action.payload.length; state.loading = false; }) diff --git a/src/types/api.ts b/src/types/api.ts index 222a082..a10211b 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -275,6 +275,7 @@ export interface RelatedGroupMemberOut { } export interface RelatedGroupDetail extends RelatedGroupOut { members: RelatedGroupMemberOut[]; + crm_id:string } export interface LegalEntityBrief { id: string; diff --git a/src/types/companyGroups/companyGroups.ts b/src/types/companyGroups/companyGroups.ts index 6c90b1c..4305314 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..4d92ea5 --- /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/useNotification.tsx b/src/utils/useNotification.tsx new file mode 100644 index 0000000..62483cf --- /dev/null +++ b/src/utils/useNotification.tsx @@ -0,0 +1,24 @@ +import { App } from "antd"; +import { 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 }), + }; +} \ No newline at end of file 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..5d53019 --- /dev/null +++ b/src/widgets/CataloguGroupTabs/CataloguGroupTabs.css @@ -0,0 +1,11 @@ + + +.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; +} \ No newline at end of file diff --git a/src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx b/src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx new file mode 100644 index 0000000..2ec78e4 --- /dev/null +++ b/src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx @@ -0,0 +1,41 @@ +import { Tabs, TabsProps } from "antd"; +import { useAppSelector } from "../../store/store"; +import { 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..e5fead2 --- /dev/null +++ b/src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx @@ -0,0 +1,40 @@ +import { Space } from "antd"; +import Title from "antd/es/typography/Title"; +import Text from "antd/es/typography/Text"; +import { useAppSelector } from "../../store/store"; + +export const CompanyGroupHeader = () => { + const activeCompanyGroup = useAppSelector( + (state) => state.company_groups.activeCompanyGroup, + ); + const count = activeCompanyGroup?.legal_entities_count ?? 0; + + const organizationWord = + count === 1 + ? "организация" + : count > 1 && count < 5 + ? "организации" + : "организаций"; + + 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 index aa56376..d7c01d2 100644 --- a/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx +++ b/src/widgets/CatalogueJurForm/CatalogueJurForm.tsx @@ -1,12 +1,13 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { Modal, Form, Input, Select, Row, Col, InputNumber } from "antd"; import { CatalogueJurFormProps } from "../../types/catalogueJur/catalogueJur"; -import { useAppDispatch, useAppSelector } from "../../store/store"; -import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks"; +import { useAppSelector } from "../../store/store"; import { fetchOneCatalogueJur } from "../../services/api/directories/catalogue_jur"; import { fetchRelatedGroups } from "../../services/api/directories/company_groups"; import { RelatedGroupOut } from "../../types/api"; -import { useMe } from "../../auth/useMe"; +import { useNotificationApi } from "../../utils/useNotification"; +import { ApiError } from "../../api/client"; +import { loadOffices } from "../Admin/UsersTable"; type Props = { open: boolean; @@ -22,25 +23,29 @@ export const CatalogueJurForm: React.FC = ({ jurId, }) => { const [form] = Form.useForm(); - const dispatch = useAppDispatch(); - const { me } = useMe(); - - const type = Form.useWatch("entity_type", form); - const activeCompanyGroup = useAppSelector( - (state) => state.company_groups.activeCompanyGroup, - ); + const notification = useNotificationApi(); const [initialDataLoading, setInitialDataLoading] = useState(false); const [gskData, setGskData] = useState([]); const [gszData, setGszData] = useState([]); - const [loadingOptions, setLoadingOptions] = useState(false); + 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() { - setInitialDataLoading(true); - form.setFieldsValue({ - company_group_id: jurId ? undefined : activeCompanyGroup?.id, - regional_office_id: me?.regional_office.id, - }); if (jurId) { + setInitialDataLoading(true); const data = await fetchOneCatalogueJur(jurId); form.setFieldsValue({ name: data?.name, @@ -50,38 +55,57 @@ export const CatalogueJurForm: React.FC = ({ 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); + 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); } async function getRelatedGroupsHandler() { - if (activeCompanyGroup?.id) { - setLoadingOptions(true); - const res = await fetchRelatedGroups(activeCompanyGroup?.id); + if (companyGroupId) { + setLoadingRelatedGroupsOptions(true); + 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); - setLoadingOptions(false); + setLoadingRelatedGroupsOptions(false); } } const handleOk = async () => { + setConfirmLoading(true); try { const values = await form.validateFields(); await onOk({ ...values, kpp: type === "ИП" ? undefined : values.kpp?.toString(), inn: jurId ? undefined : values?.inn?.toString(), + company_group_id: jurId ? undefined : values.company_group_id, }); - } catch { - return; + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } finally { + setConfirmLoading(false); } }; @@ -90,19 +114,22 @@ export const CatalogueJurForm: React.FC = ({ onCancel(); }; - useEffect(() => { - if (open) { - dispatch(getCompanyGroupsThunk()); - } - }, [open, dispatch]); + const companyGroupOptions = useMemo(() => { + return companyGrops?.map( + (company) => company && { value: company.id, label: company.name }, + ); + }, [companyGrops]); useEffect(() => { getInitialData(); - }, [open]); + }, [jurId, open]); + useEffect(() => { + getOfficesOptionsHandler(); + }, []); useEffect(() => { getRelatedGroupsHandler(); - }, [activeCompanyGroup]); + }, [companyGroupId]); return ( = ({ cancelText="Отмена" destroyOnHidden width={720} + confirmLoading={confirmLoading} loading={initialDataLoading} >
@@ -124,16 +152,23 @@ export const CatalogueJurForm: React.FC = ({ @@ -155,10 +190,7 @@ export const CatalogueJurForm: React.FC = ({ @@ -171,6 +203,7 @@ export const CatalogueJurForm: React.FC = ({ rules={[{ required: true }]} > + + + + + + + + + + + + + + + + } - placeholder={"Поиск по ЮЛ или ГСК"} + placeholder={"Поиск по ГК или CRM ID"} value={query} onChange={(e) => setQuery(e.target.value)} /> @@ -70,13 +66,15 @@ export const CompanyGroupsList = () => { style={{ marginTop: 32 }} /> ) : ( - { + { const active = item.id === activeCompanyGroup?.id; return ( - dispatch(setActiveCompanyGroup(item))} style={{ cursor: "pointer", @@ -88,6 +86,7 @@ export const CompanyGroupsList = () => { ? "inset 0 0 0 1px #2ea64b3d, inset 3px 0 0 #258141" : "inset 0 0 0 1px #0f172a14", transition: "background 0.15s", + borderBottom:0 }} onMouseEnter={(e) => { if (!active) { @@ -175,7 +174,7 @@ export const CompanyGroupsList = () => { {item.legal_entities_count}
- + ); }} /> diff --git a/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.css b/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.css new file mode 100644 index 0000000..462b562 --- /dev/null +++ b/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.css @@ -0,0 +1,65 @@ +/* Обёртка списка опций */ +.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; +} \ No newline at end of file diff --git a/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.tsx b/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.tsx new file mode 100644 index 0000000..3e805a7 --- /dev/null +++ b/src/widgets/CreateEntityPickerModal/CreateEntityPickerModal.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { Modal } from "antd"; +import { + BankOutlined, + TeamOutlined, + ApartmentOutlined, + DeploymentUnitOutlined, +} from "@ant-design/icons"; +import { 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..0e192ae --- /dev/null +++ b/src/widgets/DeleteRelatedGroupModal/DeleteRelatedGroupModal.tsx @@ -0,0 +1,33 @@ +import { Modal } from "antd"; + +type Props = { + open: boolean; + onCancel: () => void; + onOk: () => void | Promise; + loading?: boolean; +}; + +export const DeleteRelatedGroupModal: React.FC = ({ + open, + onCancel, + onOk, + loading, +}) => { + return ( + +

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

+
+ ); +}; diff --git a/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx b/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx index 861fc10..f0c4c05 100644 --- a/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx +++ b/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx @@ -6,6 +6,8 @@ import FormItem from "antd/es/form/FormItem"; import { fetchOneCatalogueJur } from "../../services/api/directories/catalogue_jur"; import dayjs from "dayjs"; import { MoveCatalogueJurProps } from "../../types/catalogueJur/catalogueJur"; +import { ApiError } from "../../api/client"; +import { useNotificationApi } from "../../utils/useNotification"; type FormValues = { name: string; @@ -30,9 +32,11 @@ export const MoveCatalogueJurForm: React.FC = ({ }) => { 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); @@ -48,11 +52,18 @@ export const MoveCatalogueJurForm: React.FC = ({ } const handleOk = async () => { + setConfirmLoading(true); try { const { effective_date, company_group_id } = await form.validateFields(); await onOk({ effective_date, company_group_id }); - } catch { - // validation error - keep modal open + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } finally { + setConfirmLoading(false); } }; @@ -76,10 +87,11 @@ export const MoveCatalogueJurForm: React.FC = ({ open={open} onCancel={handleCancel} onOk={handleOk} - okText="+ Создать" + okText="Перевести" cancelText="Отмена" destroyOnHidden loading={initialDataLoading} + confirmLoading={confirmLoading} > diff --git a/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx b/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx new file mode 100644 index 0000000..6cb6a15 --- /dev/null +++ b/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Modal, Form, Input, DatePicker, Select } from "antd"; +import FormItem from "antd/es/form/FormItem"; +import dayjs from "dayjs"; +import { RelatedGroupFormType } from "../../types/relatedGroup/relatedGroup"; +import { fetchRelatedGroup } from "../../services/api/directories/company_groups"; +import { useNotificationApi } from "../../utils/useNotification"; +import { ApiError } from "../../api/client"; +import { useAppDispatch, useAppSelector } from "../../store/store"; +import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks"; + +type Props = { + open: boolean; + onCancel: () => void; + onOk: (values: RelatedGroupFormType) => void | Promise; + relatedGroupId?: string; + kind: "gsk" | "gsz"; +}; + +export const RelatedGroupForm: React.FC = ({ + open, + onCancel, + onOk, + relatedGroupId, + kind, +}) => { + const [form] = Form.useForm(); + const notification = useNotificationApi(); + const dispatch = useAppDispatch(); + const [initialDataLoading, setInitialDataLoading] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + const companyGrops = useAppSelector((state) => state.company_groups.raw_rows); + const companyGropsLoading = useAppSelector( + (state) => state.company_groups.loading, + ); + + async function getInitialData() { + if (!relatedGroupId) return; + setInitialDataLoading(true); + try { + const data = await fetchRelatedGroup(relatedGroupId); + await dispatch(getCompanyGroupsThunk()); + form.setFieldsValue({ + crm_id: data?.crm_id, + name: data?.name, + created_date: data?.created_date, + company_group_id: data?.company_group_id, + }); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ description: e.message }); + } + } finally { + setInitialDataLoading(false); + } + } + + const handleOk = async () => { + setConfirmLoading(true); + try { + const { created_date, name, crm_id, company_group_id } = + await form.validateFields(); + await onOk({ + created_date, + name, + crm_id, + company_group_id: relatedGroupId ? undefined : company_group_id, + kind: relatedGroupId ? undefined : kind, + }); + } 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(); + }, [open]); + + return ( + + + + + + + + + +