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 }]} > = ({ notFoundContent={null} /> + + + + + + + + + + + + @@ -213,7 +278,9 @@ export const CatalogueJurForm: React.FC = ({ options={gskData?.map((g) => ({ value: g.id, label: g.name }))} showSearch optionFilterProp="label" - notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"} + notFoundContent={ + loadingRelatedGroupsOptions ? "Загрузка" : "Нет вариантов" + } /> @@ -228,7 +295,9 @@ export const CatalogueJurForm: React.FC = ({ }))} showSearch optionFilterProp="label" - notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"} + notFoundContent={ + loadingRelatedGroupsOptions ? "Загрузка" : "Нет вариантов" + } /> diff --git a/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx b/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx index bc56d1b..645fee9 100644 --- a/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx +++ b/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { ReactNode, useEffect, useMemo, useState } from "react"; import { Button, Checkbox, Dropdown, Input, Space, Tabs } from "antd"; import { useNavigate } from "react-router-dom"; import type { ColumnsType } from "antd/es/table"; @@ -10,6 +10,7 @@ import { } from "../../components/GenericTable"; import { catalogueJurConfig } from "./config"; import { + CloseOutlined, EditOutlined, EllipsisOutlined, HistoryOutlined, @@ -33,21 +34,28 @@ import { withdrawCatalogueJur, } from "../../services/api/directories/catalogue_jur"; import { WithdrawCatalogueJurForm } from "../WithdrawCatalogueJurForm/WithdrawCatalogueJurForm"; -import { setIsGettingArchive } from "../../store/slices/catalogueJurSlice/catalogueJurSlice"; +import { + setIsGettingArchive, + setRelatedGroupFilter, +} from "../../store/slices/catalogueJurSlice/catalogueJurSlice"; import { CatalogueJurForm } from "../CatalogueJurForm/CatalogueJurForm"; -import Title from "antd/es/typography/Title"; -import Text from "antd/es/typography/Text"; -import { fetchRelatedGroups } from "../../services/api/directories/company_groups"; -import { RelatedGroupOut } from "../../types/api"; +import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks"; +import { ApiError } from "../../api/client"; +import { useNotificationApi } from "../../utils/useNotification"; interface CatalogueJurTableI { outside_group: boolean; + tabsComponent?: ReactNode; } -type TabKey = "legal_entities" | "gsk" | "gsz"; -export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { +export function CatalogueJurTable({ + outside_group, + tabsComponent, +}: CatalogueJurTableI) { const navigate = useNavigate(); const dispatch = useAppDispatch(); + const notification = useNotificationApi(); + const rawRows = useAppSelector((state) => state.catalogue_jur.raw_rows); const loading = useAppSelector((state) => state.catalogue_jur.loading); const isGettingArchive = useAppSelector( @@ -57,14 +65,27 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { (state) => state.company_groups.activeCompanyGroup, ); + const relatedGroupFilter = useAppSelector( + (state) => state.catalogue_jur.relatedGroupFilter, + ); const [moveCatalogueFormOpen, setMoveCatalogueFormOpen] = useState(false); const [editCatalogueFormOpen, setEditCatalogueFormOpen] = useState(false); const [withdrawCatalogueFormOpen, setWithdrawCatalogueFormOpen] = useState(false); - const [activeTab, setActiveTab] = useState("legal_entities"); const [jurId, setJurId] = useState(""); - const [gskData, setGskData] = useState([]); - const [gszData, setGszData] = useState([]); + + const filterdData = useMemo(() => { + if (relatedGroupFilter?.id) { + return rawRows?.filter((row) => { + return row?.memberships?.some( + (member) => member.related_group_id === relatedGroupFilter.id, + ); + }); + } else { + return rawRows; + } + }, [rawRows, relatedGroupFilter]); + const { visibleKeys, setVisibleKeys } = useVisibleColumns(catalogueJurConfig); const { rawQuery, @@ -78,7 +99,7 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { } = useTableFilters(); // single filtered array — title count and table share it const { rows } = useFilteredRows( - rawRows, + filterdData, catalogueJurConfig, rawQuery, columnFilters, @@ -90,7 +111,7 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { getCatalogueJurThunk({ closed: isGettingArchive, outside_group: outside_group, - company_group_id:activeCompanyGroup?.id + company_group_id: outside_group ? undefined : activeCompanyGroup?.id, }), ); } @@ -112,9 +133,14 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { try { await moveCatalogueJur(jurId, values); getCatalogueJur(); + dispatch(getCompanyGroupsThunk()); closeMoveCatalogueJurHandler(); } catch (e) { - console.error(e); + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } } } async function withdrawCatalogueJurHandler( @@ -123,40 +149,38 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { try { await withdrawCatalogueJur(jurId, values); getCatalogueJur(); + dispatch(getCompanyGroupsThunk()); + closeWithdrawCatalogueJurHandler(); } catch (e) { - console.error(e); + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } } } async function editJurHandler(values: CatalogueJurFormProps) { try { await editCatalogueJur(jurId, values); - dispatch( - getCatalogueJurThunk({ closed: isGettingArchive, outside_group: true }), - ); + getCatalogueJur(); closeEditCatalogueJurHandler(); - } catch {} - } - - async function getRelatedGroupsHandler() { - 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); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } } } useEffect(() => { - getCatalogueJur(); + if (!!activeCompanyGroup?.id) { + getCatalogueJur(); + } }, [isGettingArchive, activeCompanyGroup]); - useEffect(() => { - getRelatedGroupsHandler(); - }, [activeCompanyGroup]); - const extraColumns: ColumnsType = useMemo( () => [ { @@ -224,22 +248,9 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { borderRadius: 12, }} > - {!outside_group && ( - - - {activeCompanyGroup?.name} - - - {activeCompanyGroup?.base_regional_office_name ?? ""} - {activeCompanyGroup?.legal_entities_count} организаций - - - )} ) : ( - setActiveTab(k as TabKey)} - style={{ marginBottom: 0 }} - items={[ - { - key: "legal_entities", - label: `ЮЛ ${activeCompanyGroup?.legal_entities_count}`, - }, - { key: "gsk", label: `ГСК ${gskData?.length ?? 0}` }, - { key: "gsz", label: `ГСЗ ${gszData?.length ?? 0}` }, - ]} - /> + tabsComponent )} @@ -281,14 +280,12 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { Сбросить фильтры )} - {activeTab === "legal_entities" && ( - dispatch(setIsGettingArchive(e.target.checked))} - > - Архив - - )} + dispatch(setIsGettingArchive(e.target.checked))} + > + Архив + + {relatedGroupFilter?.id && ( + + + + Фильтр по: + + {relatedGroupFilter.code} + + + — {filterdData?.length}{" "} + {filterdData?.length === 1 + ? "запись" + : filterdData?.length < 5 + ? "записи" + : "записей"} + + + + } + onClick={() => dispatch(setRelatedGroupFilter(null))} + style={{ + borderColor: "#d9d9d9", + color: "#262626", + }} + > + Сбросить + + + )} { closeMoveCatalogueJurHandler(); }} - onOk={(values) => { - moveCatalogueJurHandler(values); + onOk={async (values) => { + await moveCatalogueJurHandler(values); }} /> )} @@ -328,8 +376,8 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { onCancel={() => { closeWithdrawCatalogueJurHandler(); }} - onOk={(values) => { - withdrawCatalogueJurHandler(values); + onOk={async (values) => { + await withdrawCatalogueJurHandler(values); }} /> )} @@ -340,8 +388,8 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) { onCancel={() => { closeEditCatalogueJurHandler(); }} - onOk={(values) => { - editJurHandler(values); + onOk={async (values) => { + await editJurHandler(values); }} /> )} diff --git a/src/widgets/CatalogueJurTable/config.tsx b/src/widgets/CatalogueJurTable/config.tsx index 9aca89d..6fe391f 100644 --- a/src/widgets/CatalogueJurTable/config.tsx +++ b/src/widgets/CatalogueJurTable/config.tsx @@ -1,6 +1,8 @@ import { Tag } from "antd"; import type { TableConfig } from "../../components/GenericTable"; import { ICatalogueJur } from "../../types/catalogueJur/catalogueJur"; +import { TableTag } from "../../components/TableTag/TableTag"; +import { TAG_PRESETS } from "../../components/TableTag/presets"; export const catalogueJurConfig: TableConfig = { rowKey: "id", @@ -10,10 +12,15 @@ export const catalogueJurConfig: TableConfig = { key: "closed", title: "Статус", features: { sort: true }, - render: (_:unknown, row) =>{ + render: (_: unknown, row) => { // #TODO поменять логику если запись не активна - return {!row.closed ? 'Активно' : 'Не активно'} - } + return ( + + ); + }, }, { key: "inn", @@ -34,16 +41,17 @@ export const catalogueJurConfig: TableConfig = { key: "entity_type", title: "Тип", features: { multiselect: true, sort: true }, - render: (val) =>{ - return {val as string} - } + render: (val) => { + const preset = val === "ЮЛ" ? TAG_PRESETS.orange : TAG_PRESETS.purple; + return ; + }, }, { key: "regional_office", title: "Основной РФ", - render: (val:any) =>{ - return val?.name - } + render: (val: any) => { + return val?.name; + }, }, ], }; diff --git a/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx b/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx new file mode 100644 index 0000000..7c421aa --- /dev/null +++ b/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from "react"; +import { Modal, Form, Input, Select, Row, Col } from "antd"; +import { useAppSelector } from "../../store/store"; +import { loadOffices } from "../Admin/UsersTable"; +import { CreateCompanyGroupProps } from "../../types/companyGroups/companyGroups"; +import { useMe } from "../../auth/useMe"; +import { ApiError } from "../../api/client"; +import { useNotificationApi } from "../../utils/useNotification"; + +type Props = { + open: boolean; + onCancel: () => void; + onOk: (values: CreateCompanyGroupProps) => void | Promise; + companyId?: string; +}; + +export const CompanyGroupForm: React.FC = ({ + open, + onCancel, + onOk, + companyId, +}) => { + 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); + const res = await loadOffices(); + const options = res.map( + (elem) => elem && { value: elem.id, label: elem.name }, + ); + setOfficesOptions(options); + 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 ( + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/widgets/CompanyGroupsList/CompanyGroupsList.css b/src/widgets/CompanyGroupsList/CompanyGroupsList.css new file mode 100644 index 0000000..43d1896 --- /dev/null +++ b/src/widgets/CompanyGroupsList/CompanyGroupsList.css @@ -0,0 +1,4 @@ +.ant-listy-item{ + border: 0 !important; + padding: 0 !important; +} \ No newline at end of file diff --git a/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx b/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx index 978153f..8de5e4b 100644 --- a/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx +++ b/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx @@ -1,10 +1,10 @@ import { useEffect, useMemo, useState } from "react"; -import { Input, List, Spin, Empty, Typography, Tag } from "antd"; +import { Input, Spin, Empty, Typography, Tag, Listy } from "antd"; import { FolderOpenOutlined, SearchOutlined } from "@ant-design/icons"; import { useAppDispatch, useAppSelector } from "../../store/store"; import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks"; import { setActiveCompanyGroup } from "../../store/slices/companyGroupsSlice/companyGroupsSlice"; - +import "./CompanyGroupsList.css" const { Text } = Typography; export const CompanyGroupsList = () => { @@ -31,12 +31,6 @@ export const CompanyGroupsList = () => { dispatch(getCompanyGroupsThunk()); }, [dispatch]); - useEffect(() => { - if (!activeCompanyGroup && rawRows?.length) { - dispatch(setActiveCompanyGroup(rawRows?.[0])); - } - }, [rawRows, activeCompanyGroup]); - return ( { borderRadius: 12, }} > - + } - 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) => ( + onSelect(opt.key)} + > + {opt.icon} + + {opt.title} + {opt.description} + + + ))} + + + ); +}; 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 ( + + + + + + + + + + + + ({ + value: value ? dayjs(value) : undefined, + })} + normalize={(value) => + value ? value.format("YYYY-MM-DD") : undefined + } + > + + + + + ); +}; diff --git a/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx b/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx new file mode 100644 index 0000000..af27b3c --- /dev/null +++ b/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx @@ -0,0 +1,222 @@ +import { ReactNode, useMemo, useState } from "react"; +import { Button, Dropdown, Space } from "antd"; +import { useNavigate } from "react-router-dom"; +import type { ColumnsType } from "antd/es/table"; +import { + DataTable, + ColumnSettings, + useVisibleColumns, + useFilteredRows, +} from "../../components/GenericTable"; +import { EllipsisOutlined } from "@ant-design/icons"; +import { useTableFilters } from "../../components/GenericTable/useTableFilters"; +import { RelatedGroupOut } from "../../types/api"; +import { relateGroupConfig } from "./config"; +import { RelatedGroupForm } from "../RelatedGroupForm/RelatedGroupForm"; +import { RelatedGroupFormType } from "../../types/relatedGroup/relatedGroup"; +import { + deleteRelatedGroup, + editRelatedGroup, +} from "../../services/api/directories/related-groups"; +import { DeleteRelatedGroupModal } from "../DeleteRelatedGroupModal/DeleteRelatedGroupModal"; +import { useNotificationApi } from "../../utils/useNotification"; +import { ApiError } from "../../api/client"; + +interface RelatedGroupTableI { + tabsComponent?: ReactNode; + data: RelatedGroupOut[]; + loading: boolean; + getData: () => void; + activeTab: "gsk" | "gsz"; +} + +export function RelatedGroupTable({ + tabsComponent, + data, + loading, + getData, + activeTab, +}: RelatedGroupTableI) { + const navigate = useNavigate(); + const notification = useNotificationApi(); + + const [editFormOpen, setEditFormOpen] = useState(false); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [deletingLoading, setDeletingLoading] = useState(false); + const [groupId, setGroupId] = useState(""); + + const { visibleKeys, setVisibleKeys } = useVisibleColumns(relateGroupConfig); + const { + rawQuery, + columnFilters, + setColumnFilters, + sort, + setSort, + resetAll, + hasActiveFilters, + } = useTableFilters(); + // single filtered array — title count and table share it + const { rows } = useFilteredRows( + data, + relateGroupConfig, + rawQuery, + columnFilters, + sort, + ); + + function closeEditRelatedGroupHandler() { + setGroupId(""); + setEditFormOpen(false); + } + function closeDeleteRelatedGroupHandler() { + setGroupId(""); + setDeleteModalOpen(false); + } + + async function deleteRelatedGroupHandler() { + try { + setDeletingLoading(true); + await deleteRelatedGroup(groupId); + getData(); + setDeleteModalOpen(false); + setGroupId(""); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } finally { + setDeletingLoading(false); + } + } + + async function editRelatedGroupHandler(values: RelatedGroupFormType) { + try { + await editRelatedGroup(groupId, values); + getData(); + closeEditRelatedGroupHandler(); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } + } + + const extraColumns: ColumnsType = useMemo( + () => [ + { + key: "actions", + title: "", + width: 30, + render: (_: unknown, row: RelatedGroupOut) => { + const items = [ + { + key: "edit", + label: "Изменить", + onClick: () => { + setGroupId(row.id); + setEditFormOpen(true); + }, + }, + { + key: "delete", + label: "Удалить", + danger: true, + onClick: () => { + setGroupId(row.id); + setDeleteModalOpen(true); + }, + }, + ]; + return ( + + + + + + ); + }, + }, + ], + [navigate], + ); + + return ( + + + {tabsComponent} + + + {hasActiveFilters && ( + { + resetAll(); + }} + > + Сбросить фильтры + + )} + + + + + {editFormOpen && ( + { + closeEditRelatedGroupHandler(); + }} + onOk={async (values) => { + await editRelatedGroupHandler(values); + }} + /> + )} + {deleteModalOpen && ( + closeDeleteRelatedGroupHandler()} + onOk={deleteRelatedGroupHandler} + /> + )} + + ); +} diff --git a/src/widgets/RelatedGroupTable/config.tsx b/src/widgets/RelatedGroupTable/config.tsx new file mode 100644 index 0000000..e57fb8a --- /dev/null +++ b/src/widgets/RelatedGroupTable/config.tsx @@ -0,0 +1,40 @@ +import type { TableConfig } from "../../components/GenericTable"; +import { RelatedGroupOut } from "../../types/api"; +import { store } from "../../store/store"; +import { setRelatedGroupFilter } from "../../store/slices/catalogueJurSlice/catalogueJurSlice"; +import { Button } from "antd"; +import { VerticalAlignTopOutlined } from "@ant-design/icons"; +export const relateGroupConfig: TableConfig = { + rowKey: "id", + searchableKeys: ["code", "name", "members_count", "created_date"], + columns: [ + { + key: "code", + title: "ID", + features: { search: true, multiselect: true }, + }, + { + key: "name", + title: "Наименование", + features: { search: true, multiselect: true }, + render:(val, row)=>{ + const onClick = () => store.dispatch(setRelatedGroupFilter(row)); + return( + } iconPlacement="end" type="link" onClick={onClick}>{val} + ) + } + }, + { + key: "members_count", + title: "Число участников", + features: { search: true, multiselect: true, sort: true }, + }, + { + key: "created_date", + title: "Дата создания", + render: (val) => { + return new Date(val as string).toLocaleDateString("ru-RU"); + }, + }, + ], +}; diff --git a/src/widgets/WithdrawCatalogueJurForm/WithdrawCatalogueJurForm.tsx b/src/widgets/WithdrawCatalogueJurForm/WithdrawCatalogueJurForm.tsx index 82a9e82..850c97b 100644 --- a/src/widgets/WithdrawCatalogueJurForm/WithdrawCatalogueJurForm.tsx +++ b/src/widgets/WithdrawCatalogueJurForm/WithdrawCatalogueJurForm.tsx @@ -5,6 +5,8 @@ import { fetchOneCatalogueJur } from "../../services/api/directories/catalogue_j import dayjs from "dayjs"; import { WithdrawCatalogueJurProps } from "../../types/catalogueJur/catalogueJur"; import TextArea from "antd/es/input/TextArea"; +import { ApiError } from "../../api/client"; +import { useNotificationApi } from "../../utils/useNotification"; interface FormValues extends WithdrawCatalogueJurProps { name: string; @@ -24,6 +26,9 @@ export const WithdrawCatalogueJurForm: React.FC = ({ jurId, }) => { const [form] = Form.useForm(); + const notification = useNotificationApi(); + + const [confirmLoading, setConfirmLoading] = useState(false); const [initialDataLoading, setInitialDataLoading] = useState(false); const eventOptions = [ @@ -44,6 +49,7 @@ export const WithdrawCatalogueJurForm: React.FC = ({ } const handleOk = async () => { + setConfirmLoading(true) try { const { effective_date, @@ -51,8 +57,14 @@ export const WithdrawCatalogueJurForm: React.FC = ({ comment = "", } = await form.validateFields(); await onOk({ effective_date, event, comment }); - } catch { - // validation error - keep modal open + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } finally{ + setConfirmLoading(false) } }; @@ -75,6 +87,7 @@ export const WithdrawCatalogueJurForm: React.FC = ({ cancelText="Отмена" destroyOnHidden loading={initialDataLoading} + confirmLoading={confirmLoading} okButtonProps={{ danger: true }} > @@ -106,13 +119,14 @@ export const WithdrawCatalogueJurForm: React.FC = ({ } > - +
+ Группа связанных компаний будет удалена. +