updated company group and forms
This commit is contained in:
parent
83d4c2b894
commit
71dae2173d
108
src/App.tsx
108
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 (
|
||||
<Provider store={store}>
|
||||
<BrowserRouter basename={Config.APP_ADDRESS}>
|
||||
<AuthGate>
|
||||
<Layout hasSider={true} style={{ height: '100vh' }}>
|
||||
<Sidebar />
|
||||
<Layout.Content>
|
||||
<Routes>
|
||||
<Route path='/admin' element={<Admin />} />
|
||||
<Route path='/' element={<Main />} />
|
||||
<Route path='/groups'>
|
||||
<Route index element={<GroupsLayout />} />
|
||||
<Route path=':group_id' element={<ConsolidationPage />} />
|
||||
<Route path=':group_id/versions/:version_id/osv' element={<ConsolidatedOsvPage />} />
|
||||
<Route path='monozaemchiki' element={<Mono />} />
|
||||
</Route>
|
||||
<Route path='/catalogue/groups' element={<CatalogueGroup />} />
|
||||
<Route path='/catalogue/jur_l' element={<CatalogueJur />} />
|
||||
<Route path='/docstorage' element={<DocStorage />} />
|
||||
<Route path='/documents/:document_id' element={<DocumentPage />} />
|
||||
<Route path='/logging' element={<AuditLogging />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</AuthGate>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AntApp>
|
||||
<BrowserRouter basename={Config.APP_ADDRESS}>
|
||||
<AuthGate>
|
||||
<Layout hasSider={true} style={{ height: "100vh" }}>
|
||||
<Sidebar />
|
||||
<Layout.Content>
|
||||
<Routes>
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/" element={<Main />} />
|
||||
<Route path="/groups">
|
||||
<Route index element={<GroupsLayout />} />
|
||||
<Route path=":group_id" element={<ConsolidationPage />} />
|
||||
<Route
|
||||
path=":group_id/versions/:version_id/osv"
|
||||
element={<ConsolidatedOsvPage />}
|
||||
/>
|
||||
<Route path="monozaemchiki" element={<Mono />} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/catalogue/groups"
|
||||
element={<CatalogueGroup />}
|
||||
/>
|
||||
<Route path="/catalogue/jur_l" element={<CatalogueJur />} />
|
||||
<Route path="/docstorage" element={<DocStorage />} />
|
||||
<Route
|
||||
path="/documents/:document_id"
|
||||
element={<DocumentPage />}
|
||||
/>
|
||||
<Route path="/logging" element={<AuditLogging />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</AuthGate>
|
||||
</BrowserRouter>
|
||||
</AntApp>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@ -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<CreateEntityKind | null>(null);
|
||||
const [relatedGroupLoading, setRelatedGroupLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] =
|
||||
useState<CatalogueGroupTabKey>("legal_entities");
|
||||
const [gskData, setGskData] = useState<RelatedGroupOut[]>([]);
|
||||
const [gszData, setGszData] = useState<RelatedGroupOut[]>([]);
|
||||
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 (
|
||||
<div
|
||||
@ -45,7 +153,7 @@ export function CatalogueGroup() {
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateFormOpen(true)}
|
||||
onClick={() => setEntityCreateModalOpen(true)}
|
||||
>
|
||||
Создать ГК/ЮЛ
|
||||
</Button>
|
||||
@ -59,21 +167,94 @@ export function CatalogueGroup() {
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 320, flexShrink: 0, minHeight: 0 }}>
|
||||
<CompanyGroupsList/>
|
||||
<CompanyGroupsList />
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0, minHeight: 0, borderRadius:12, background:'white' }}>
|
||||
<CatalogueJurTable outside_group={false} />
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
borderRadius: 12,
|
||||
background: "white",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<CompanyGroupHeader />
|
||||
{activeTab === "legal_entities" && (
|
||||
<CatalogueJurTable
|
||||
outside_group={false}
|
||||
tabsComponent={
|
||||
<CataloguGroupTabs
|
||||
activeTab={activeTab}
|
||||
setActiveTab={(val) => setActiveTab(val)}
|
||||
gskData={gskData}
|
||||
gszData={gszData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(activeTab === "gsk" || activeTab === "gsz") && (
|
||||
<RelatedGroupTable
|
||||
activeTab={activeTab}
|
||||
data={activeTab === "gsz" ? gszData : gskData}
|
||||
getData={() => getRelatedGroupsHandler()}
|
||||
tabsComponent={
|
||||
<CataloguGroupTabs
|
||||
activeTab={activeTab}
|
||||
setActiveTab={(val) => setActiveTab(val)}
|
||||
gskData={gskData}
|
||||
gszData={gszData}
|
||||
/>
|
||||
}
|
||||
loading={relatedGroupLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{createFormOpen && (
|
||||
<CreateEntityPickerModal
|
||||
open={entityCreateModalOpen}
|
||||
onCancel={() => {
|
||||
setEntityCreateModalOpen(false);
|
||||
}}
|
||||
onSelect={(kind) => {
|
||||
setKindForm(kind);
|
||||
setEntityCreateModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
{kindForm === "ul" && (
|
||||
<CatalogueJurForm
|
||||
open={createFormOpen}
|
||||
open={kindForm === "ul"}
|
||||
onCancel={() => {
|
||||
setCreateFormOpen(false);
|
||||
setKindForm(null);
|
||||
}}
|
||||
onOk={(values) => {
|
||||
createJurHandler(values);
|
||||
onOk={async (values) => {
|
||||
await createJurHandler(values);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(kindForm === "gsk" || kindForm === "gsz") && (
|
||||
<RelatedGroupForm
|
||||
kind={kindForm}
|
||||
open={kindForm === "gsk" || kindForm === "gsz"}
|
||||
onCancel={() => {
|
||||
setKindForm(null);
|
||||
}}
|
||||
onOk={async (values) => {
|
||||
await createRelatedGroupHandler(values);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{kindForm === "gk" && (
|
||||
<CompanyGroupForm
|
||||
open={kindForm === "gk"}
|
||||
onCancel={() => {
|
||||
setKindForm(null);
|
||||
}}
|
||||
onOk={async (values) => {
|
||||
await createCompanyGroupHandler(values);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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() {
|
||||
Создать ЮЛ
|
||||
</Button>
|
||||
</Space>
|
||||
<CatalogueJurTable outside_group={outside_group}/>
|
||||
<CatalogueJurTable outside_group={outside_group} />
|
||||
{createFormOpen && (
|
||||
<CatalogueJurForm
|
||||
open={createFormOpen}
|
||||
onCancel={() => {
|
||||
setCreateFormOpen(false);
|
||||
}}
|
||||
onOk={(values) => {
|
||||
createJurHandler(values);
|
||||
onOk={async(values) => {
|
||||
await createJurHandler(values);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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<LegalEntit
|
||||
}
|
||||
|
||||
export const fetchLegalEntity = (id: string) => api.get<LegalEntityBrief>(`/directories/legal-entities/${id}`);
|
||||
|
||||
export async function createCompanyGroup(
|
||||
body: CreateCompanyGroupProps,
|
||||
): Promise<void> {
|
||||
return api.post<void>(
|
||||
`/directories/company-groups`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
23
src/services/api/directories/related-groups.ts
Normal file
23
src/services/api/directories/related-groups.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { api } from "../../../api/client";
|
||||
import { RelatedGroupFormType } from "../../../types/relatedGroup/relatedGroup";
|
||||
|
||||
export async function editRelatedGroup(
|
||||
groupId: string,
|
||||
body: RelatedGroupFormType,
|
||||
): Promise<void> {
|
||||
return api.patch<void>(
|
||||
`/directories/related-groups/${groupId}`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
export async function createRelatedGroup(
|
||||
companyGroupId: string,
|
||||
body: RelatedGroupFormType,
|
||||
): Promise<void> {
|
||||
return api.post<void>(
|
||||
`/directories/company-groups/${companyGroupId}/related-groups`,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
export const deleteRelatedGroup = (id: string) => api.del<void>(`/directories/related-groups/${id}`);
|
||||
@ -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<boolean>) {
|
||||
state.isGettingArchive = action.payload;
|
||||
},
|
||||
setRelatedGroupFilter(state, action: PayloadAction<RelatedGroupOut | null>) {
|
||||
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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
})
|
||||
|
||||
@ -275,6 +275,7 @@ export interface RelatedGroupMemberOut {
|
||||
}
|
||||
export interface RelatedGroupDetail extends RelatedGroupOut {
|
||||
members: RelatedGroupMemberOut[];
|
||||
crm_id:string
|
||||
}
|
||||
export interface LegalEntityBrief {
|
||||
id: string;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
9
src/types/relatedGroup/relatedGroup.ts
Normal file
9
src/types/relatedGroup/relatedGroup.ts
Normal file
@ -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";
|
||||
24
src/utils/useNotification.tsx
Normal file
24
src/utils/useNotification.tsx
Normal file
@ -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 }),
|
||||
};
|
||||
}
|
||||
@ -12,7 +12,7 @@ const USERS_LIMIT = 200;
|
||||
// Загрузчики — функции уровня модуля: они не меняются между отрисовками (требование useAsyncData)
|
||||
const loadUsers = () => api.get<Page<AdminUser>>(`/admin/users?limit=${USERS_LIMIT}`);
|
||||
const loadRoles = () => api.get<Role[]>('/admin/roles');
|
||||
const loadOffices = () => api.get<RegionalOffice[]>('/directories/regional-offices');
|
||||
export const loadOffices = () => api.get<RegionalOffice[]>('/directories/regional-offices');
|
||||
|
||||
/**
|
||||
* Таблица пользователей: роли меняются прямо в строке (`PUT /admin/users/{id}/roles`), активность — переключателем
|
||||
|
||||
11
src/widgets/CataloguGroupTabs/CataloguGroupTabs.css
Normal file
11
src/widgets/CataloguGroupTabs/CataloguGroupTabs.css
Normal file
@ -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;
|
||||
}
|
||||
41
src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx
Normal file
41
src/widgets/CataloguGroupTabs/CataloguGroupTabs.tsx
Normal file
@ -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 (
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => 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}` },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
40
src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx
Normal file
40
src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
style={{
|
||||
padding: "6px 16px",
|
||||
borderBottom: "1px solid #0f172a0f",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
{activeCompanyGroup?.name ?? ""}
|
||||
</Title>
|
||||
<Space>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{activeCompanyGroup?.base_regional_office_name ?? ""}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{count ?? 0} {organizationWord}
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -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<Props> = ({
|
||||
jurId,
|
||||
}) => {
|
||||
const [form] = Form.useForm<CatalogueJurFormProps>();
|
||||
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<RelatedGroupOut[]>([]);
|
||||
const [gszData, setGszData] = useState<RelatedGroupOut[]>([]);
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
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 (
|
||||
<Modal
|
||||
@ -116,6 +143,7 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
||||
cancelText="Отмена"
|
||||
destroyOnHidden
|
||||
width={720}
|
||||
confirmLoading={confirmLoading}
|
||||
loading={initialDataLoading}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
@ -124,16 +152,23 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
||||
<Form.Item
|
||||
name="inn"
|
||||
label="ИНН"
|
||||
rules={[
|
||||
{ required: true, message: "Укажите ИНН" },
|
||||
{ pattern: /^\d{10}$/, message: "10 цифр" },
|
||||
]}
|
||||
rules={
|
||||
type === "ИП"
|
||||
? [
|
||||
{ required: true, message: "Укажите ИНН" },
|
||||
{ pattern: /^\d{12}$/, message: "12 цифр" },
|
||||
]
|
||||
: [
|
||||
{ required: true, message: "Укажите ИНН" },
|
||||
{ pattern: /^\d{10}$/, message: "10 цифр" },
|
||||
]
|
||||
}
|
||||
>
|
||||
<InputNumber
|
||||
disabled={!!jurId}
|
||||
style={{ width: "100%" }}
|
||||
placeholder="0000000000"
|
||||
maxLength={10}
|
||||
placeholder={type === "ИП" ? "000000000000" : "0000000000"}
|
||||
maxLength={type === "ИП" ? 12 : 10}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@ -155,10 +190,7 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
||||
<Form.Item
|
||||
name="crm_id"
|
||||
label="ID ЮЛ в CRM"
|
||||
rules={[
|
||||
{ required: true, message: "Укажите CRM ID" },
|
||||
{ pattern: /^\d{11}$/, message: "11 цифр" },
|
||||
]}
|
||||
rules={[{ len: 11, message: "Должно быть 11 символов" }]}
|
||||
>
|
||||
<Input maxLength={11} />
|
||||
</Form.Item>
|
||||
@ -171,6 +203,7 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
disabled={!!jurId}
|
||||
options={[
|
||||
{ value: "ЮЛ", label: "ЮЛ" },
|
||||
{ value: "ИП", label: "ИП" },
|
||||
@ -203,6 +236,38 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
||||
notFoundContent={null}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="regional_office_id" label="Базовый РФ ГК">
|
||||
<Select
|
||||
allowClear
|
||||
loading={loadingOfficesOptions}
|
||||
placeholder="— не указан —"
|
||||
options={officesOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={
|
||||
loadingOfficesOptions ? "Загрузка" : "Нет вариантов"
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="company_group_id" label="ГК">
|
||||
<Select
|
||||
disabled={!!jurId}
|
||||
allowClear
|
||||
placeholder="— не указан —"
|
||||
options={companyGroupOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={
|
||||
companyGropsLoading ? "Загрузка" : "Нет вариантов"
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
@ -213,7 +278,9 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
||||
options={gskData?.map((g) => ({ value: g.id, label: g.name }))}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"}
|
||||
notFoundContent={
|
||||
loadingRelatedGroupsOptions ? "Загрузка" : "Нет вариантов"
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@ -228,7 +295,9 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
||||
}))}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"}
|
||||
notFoundContent={
|
||||
loadingRelatedGroupsOptions ? "Загрузка" : "Нет вариантов"
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
@ -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<TabKey>("legal_entities");
|
||||
const [jurId, setJurId] = useState("");
|
||||
const [gskData, setGskData] = useState<RelatedGroupOut[]>([]);
|
||||
const [gszData, setGszData] = useState<RelatedGroupOut[]>([]);
|
||||
|
||||
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<ICatalogueJur>();
|
||||
// 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<ICatalogueJur> = useMemo(
|
||||
() => [
|
||||
{
|
||||
@ -224,22 +248,9 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
{!outside_group && (
|
||||
<div
|
||||
style={{ padding: "12px 16px", borderBottom: "1px solid #0f172a0f" }}
|
||||
>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
{activeCompanyGroup?.name}
|
||||
</Title>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{activeCompanyGroup?.base_regional_office_name ?? ""}
|
||||
{activeCompanyGroup?.legal_entities_count} организаций
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<Space
|
||||
style={{
|
||||
padding: "0 12px",
|
||||
padding: `${outside_group ? "12px" : "0px"} 12px`,
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px",
|
||||
@ -255,19 +266,7 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
||||
style={{ maxWidth: 320 }}
|
||||
/>
|
||||
) : (
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(k) => 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
|
||||
)}
|
||||
|
||||
<Space>
|
||||
@ -281,14 +280,12 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
||||
Сбросить фильтры
|
||||
</Button>
|
||||
)}
|
||||
{activeTab === "legal_entities" && (
|
||||
<Checkbox
|
||||
checked={isGettingArchive}
|
||||
onChange={(e) => dispatch(setIsGettingArchive(e.target.checked))}
|
||||
>
|
||||
Архив
|
||||
</Checkbox>
|
||||
)}
|
||||
<Checkbox
|
||||
checked={isGettingArchive}
|
||||
onChange={(e) => dispatch(setIsGettingArchive(e.target.checked))}
|
||||
>
|
||||
Архив
|
||||
</Checkbox>
|
||||
<ColumnSettings
|
||||
defs={catalogueJurConfig.columns}
|
||||
visibleKeys={visibleKeys}
|
||||
@ -296,6 +293,57 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
{relatedGroupFilter?.id && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 16,
|
||||
padding: "10px 16px",
|
||||
background: "#fff8e1", // светло-жёлтый фон
|
||||
borderTop: "1px solid #ffe58f",
|
||||
borderBottom: "1px solid #ffe58f",
|
||||
borderRadius: 8,
|
||||
color: "#874d00",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<SearchOutlined style={{ fontSize: 16, color: "#874d00" }} />
|
||||
<span style={{ color: "#874d00" }}>Фильтр по:</span>
|
||||
<span style={{ fontWeight: 600, color: "#ad4e00" }}>
|
||||
{relatedGroupFilter.code}
|
||||
</span>
|
||||
<span style={{ color: "#874d00" }}>
|
||||
— {filterdData?.length}{" "}
|
||||
{filterdData?.length === 1
|
||||
? "запись"
|
||||
: filterdData?.length < 5
|
||||
? "записи"
|
||||
: "записей"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => dispatch(setRelatedGroupFilter(null))}
|
||||
style={{
|
||||
borderColor: "#d9d9d9",
|
||||
color: "#262626",
|
||||
}}
|
||||
>
|
||||
Сбросить
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<DataTable
|
||||
sortState={sort}
|
||||
onSortChange={setSort}
|
||||
@ -316,8 +364,8 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
||||
onCancel={() => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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<ICatalogueJur> = {
|
||||
rowKey: "id",
|
||||
@ -10,10 +12,15 @@ export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
|
||||
key: "closed",
|
||||
title: "Статус",
|
||||
features: { sort: true },
|
||||
render: (_:unknown, row) =>{
|
||||
render: (_: unknown, row) => {
|
||||
// #TODO поменять логику если запись не активна
|
||||
return <Tag variant="outlined" color={'success'}>{!row.closed ? 'Активно' : 'Не активно'}</Tag>
|
||||
}
|
||||
return (
|
||||
<TableTag
|
||||
text={!row.closed ? "Активно" : "Не активно"}
|
||||
{...TAG_PRESETS.green}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "inn",
|
||||
@ -34,16 +41,17 @@ export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
|
||||
key: "entity_type",
|
||||
title: "Тип",
|
||||
features: { multiselect: true, sort: true },
|
||||
render: (val) =>{
|
||||
return <Tag variant="outlined" color={val === 'ЮЛ' ? 'purple' : 'orange'}>{val as string}</Tag>
|
||||
}
|
||||
render: (val) => {
|
||||
const preset = val === "ЮЛ" ? TAG_PRESETS.orange : TAG_PRESETS.purple;
|
||||
return <TableTag text={val as string} {...preset} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "regional_office",
|
||||
title: "Основной РФ",
|
||||
render: (val:any) =>{
|
||||
return val?.name
|
||||
}
|
||||
render: (val: any) => {
|
||||
return val?.name;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
124
src/widgets/CompanyGroupForm/CompanyGroupForm.tsx
Normal file
124
src/widgets/CompanyGroupForm/CompanyGroupForm.tsx
Normal file
@ -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<void>;
|
||||
companyId?: string;
|
||||
};
|
||||
|
||||
export const CompanyGroupForm: React.FC<Props> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onOk,
|
||||
companyId,
|
||||
}) => {
|
||||
const [form] = Form.useForm<CreateCompanyGroupProps>();
|
||||
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 (
|
||||
<Modal
|
||||
title={"Создать группу компаний"}
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
onOk={handleOk}
|
||||
okText={"Создать"}
|
||||
cancelText="Отмена"
|
||||
destroyOnHidden
|
||||
confirmLoading={confirmLoading}
|
||||
width={720}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Название ГК"
|
||||
rules={[{ required: true, message: "Укажите название ГК" }]}
|
||||
>
|
||||
<Input placeholder="Например: Агроком" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="crm_id"
|
||||
label="ID ГК в CRM"
|
||||
rules={[{ len: 11, message: "Должно быть 11 символов" }]}
|
||||
>
|
||||
<Input maxLength={11} placeholder="Генерируется автоматически" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="base_regional_office_id" label="Базовый РФ ГК">
|
||||
<Select
|
||||
allowClear
|
||||
loading={loadingOptions}
|
||||
placeholder="— не указан —"
|
||||
options={officesOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
4
src/widgets/CompanyGroupsList/CompanyGroupsList.css
Normal file
4
src/widgets/CompanyGroupsList/CompanyGroupsList.css
Normal file
@ -0,0 +1,4 @@
|
||||
.ant-listy-item{
|
||||
border: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
@ -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 (
|
||||
<div
|
||||
style={{
|
||||
@ -48,11 +42,13 @@ export const CompanyGroupsList = () => {
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "12px 16px", borderBottom:"1px solid #0f172a0f" }}>
|
||||
<div
|
||||
style={{ padding: "12px 16px", borderBottom: "1px solid #0f172a0f" }}
|
||||
>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined style={{ color: "#bfbfbf" }} />}
|
||||
placeholder={"Поиск по ЮЛ или ГСК"}
|
||||
placeholder={"Поиск по ГК или CRM ID"}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
@ -70,13 +66,15 @@ export const CompanyGroupsList = () => {
|
||||
style={{ marginTop: 32 }}
|
||||
/>
|
||||
) : (
|
||||
<List
|
||||
dataSource={filtered}
|
||||
split={false}
|
||||
renderItem={(item) => {
|
||||
<Listy
|
||||
virtual
|
||||
rowKey={"id"}
|
||||
items={filtered}
|
||||
height={900}
|
||||
itemRender={(item) => {
|
||||
const active = item.id === activeCompanyGroup?.id;
|
||||
return (
|
||||
<List.Item
|
||||
<div
|
||||
onClick={() => 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}
|
||||
</Tag>
|
||||
</div>
|
||||
</List.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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: <BankOutlined />,
|
||||
},
|
||||
{
|
||||
key: "ul",
|
||||
title: "ЮЛ",
|
||||
description: "Юридическое лицо",
|
||||
icon: <TeamOutlined />,
|
||||
},
|
||||
{
|
||||
key: "gsk",
|
||||
title: "ГСК",
|
||||
description: "Группа связанных компаний",
|
||||
icon: <ApartmentOutlined />,
|
||||
},
|
||||
{
|
||||
key: "gsz",
|
||||
title: "ГСЗ",
|
||||
description: "Группа связанных заёмщиков",
|
||||
icon: <DeploymentUnitOutlined />,
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onSelect: (kind: CreateEntityKind) => void;
|
||||
};
|
||||
|
||||
export const CreateEntityPickerModal: React.FC<Props> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onSelect,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title="Выберите объект для добавления"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
centered
|
||||
destroyOnHidden
|
||||
styles={{
|
||||
header: {
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
paddingBottom: 16,
|
||||
marginBottom: 0,
|
||||
},
|
||||
body: { paddingTop: 16, paddingBottom: 16 },
|
||||
}}
|
||||
>
|
||||
<div className="options">
|
||||
{OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.key}
|
||||
type="button"
|
||||
className="optionCard"
|
||||
onClick={() => onSelect(opt.key)}
|
||||
>
|
||||
<div className="optionIcon">{opt.icon}</div>
|
||||
<div className="optionText">
|
||||
<div className="optionTitle">{opt.title}</div>
|
||||
<div className="optionDescription">{opt.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { Modal } from "antd";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: () => void | Promise<void>;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export const DeleteRelatedGroupModal: React.FC<Props> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onOk,
|
||||
loading,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title="Удалить?"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
onOk={onOk}
|
||||
okText="Подтвердить"
|
||||
cancelText="Отмена"
|
||||
okButtonProps={{ danger: true }}
|
||||
confirmLoading={loading}
|
||||
centered
|
||||
>
|
||||
<p style={{ margin: 0, color: "#595959" }}>
|
||||
Группа связанных компаний будет удалена.
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -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<Props> = ({
|
||||
}) => {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
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<Props> = ({
|
||||
}
|
||||
|
||||
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<Props> = ({
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
onOk={handleOk}
|
||||
okText="+ Создать"
|
||||
okText="Перевести"
|
||||
cancelText="Отмена"
|
||||
destroyOnHidden
|
||||
loading={initialDataLoading}
|
||||
confirmLoading={confirmLoading}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<FormItem name="name" label="ЮЛ">
|
||||
|
||||
158
src/widgets/RelatedGroupForm/RelatedGroupForm.tsx
Normal file
158
src/widgets/RelatedGroupForm/RelatedGroupForm.tsx
Normal file
@ -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<void>;
|
||||
relatedGroupId?: string;
|
||||
kind: "gsk" | "gsz";
|
||||
};
|
||||
|
||||
export const RelatedGroupForm: React.FC<Props> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onOk,
|
||||
relatedGroupId,
|
||||
kind,
|
||||
}) => {
|
||||
const [form] = Form.useForm<RelatedGroupFormType>();
|
||||
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 (
|
||||
<Modal
|
||||
title={`${relatedGroupId ? "Изменить" : "Создать"} группу связанных ${kind === "gsk" ? "компаний" : "заемщииков"}`}
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
onOk={handleOk}
|
||||
okText={`${relatedGroupId ? "Сохранить" : "Создать"}`}
|
||||
cancelText="Отмена"
|
||||
destroyOnHidden
|
||||
loading={initialDataLoading}
|
||||
confirmLoading={confirmLoading}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<FormItem
|
||||
name="crm_id"
|
||||
label="ID в CRM"
|
||||
rules={[{ len: 11, message: "Должно быть 11 символов" }]}
|
||||
>
|
||||
<Input maxLength={11} placeholder="Генерируется автоматически" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="name"
|
||||
label="Наименование"
|
||||
rules={[{ required: true, message: "Укажите наименование" }]}
|
||||
>
|
||||
<Input />
|
||||
</FormItem>
|
||||
<Form.Item
|
||||
name="company_group_id"
|
||||
label="ГК"
|
||||
rules={[{ required: true, message: "Выберите ГК" }]}
|
||||
>
|
||||
<Select
|
||||
disabled={!!relatedGroupId}
|
||||
allowClear
|
||||
loading={companyGropsLoading}
|
||||
options={companyGroupOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={companyGropsLoading ? "Загрузка" : "Нет вариантов"}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="created_date"
|
||||
label="Дата создания"
|
||||
rules={[{ required: true, message: "Укажите дату" }]}
|
||||
getValueProps={(value: string | undefined) => ({
|
||||
value: value ? dayjs(value) : undefined,
|
||||
})}
|
||||
normalize={(value) =>
|
||||
value ? value.format("YYYY-MM-DD") : undefined
|
||||
}
|
||||
>
|
||||
<DatePicker
|
||||
style={{ width: "100%" }}
|
||||
format="DD.MM.YYYY"
|
||||
placeholder="ДД.ММ.ГГГГ"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
222
src/widgets/RelatedGroupTable/RelatedGroupTable.tsx
Normal file
222
src/widgets/RelatedGroupTable/RelatedGroupTable.tsx
Normal file
@ -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<RelatedGroupOut>();
|
||||
// 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<RelatedGroupOut> = 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 (
|
||||
<Dropdown menu={{ items }} trigger={["click"]}>
|
||||
<Button type="default" style={{ width: "32px" }}>
|
||||
<EllipsisOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[navigate],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
background: "white",
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<Space
|
||||
style={{
|
||||
padding: "0 12px",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px",
|
||||
}}
|
||||
>
|
||||
{tabsComponent}
|
||||
|
||||
<Space>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
resetAll();
|
||||
}}
|
||||
>
|
||||
Сбросить фильтры
|
||||
</Button>
|
||||
)}
|
||||
<ColumnSettings
|
||||
defs={relateGroupConfig.columns}
|
||||
visibleKeys={visibleKeys}
|
||||
onChange={setVisibleKeys}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
<DataTable
|
||||
sortState={sort}
|
||||
onSortChange={setSort}
|
||||
config={relateGroupConfig}
|
||||
loading={loading}
|
||||
rows={rows}
|
||||
allRows={data}
|
||||
visibleKeys={visibleKeys}
|
||||
globalQuery={rawQuery}
|
||||
columnFilters={columnFilters}
|
||||
onColumnFiltersChange={setColumnFilters}
|
||||
extraColumns={extraColumns}
|
||||
/>
|
||||
{editFormOpen && (
|
||||
<RelatedGroupForm
|
||||
kind={activeTab}
|
||||
relatedGroupId={groupId}
|
||||
open={editFormOpen}
|
||||
onCancel={() => {
|
||||
closeEditRelatedGroupHandler();
|
||||
}}
|
||||
onOk={async (values) => {
|
||||
await editRelatedGroupHandler(values);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteModalOpen && (
|
||||
<DeleteRelatedGroupModal
|
||||
open={deleteModalOpen}
|
||||
loading={deletingLoading}
|
||||
onCancel={() => closeDeleteRelatedGroupHandler()}
|
||||
onOk={deleteRelatedGroupHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/widgets/RelatedGroupTable/config.tsx
Normal file
40
src/widgets/RelatedGroupTable/config.tsx
Normal file
@ -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<RelatedGroupOut> = {
|
||||
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(
|
||||
<Button icon={<VerticalAlignTopOutlined />} iconPlacement="end" type="link" onClick={onClick}>{val}</Button>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
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");
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -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<Props> = ({
|
||||
jurId,
|
||||
}) => {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const notification = useNotificationApi();
|
||||
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const [initialDataLoading, setInitialDataLoading] = useState(false);
|
||||
|
||||
const eventOptions = [
|
||||
@ -44,6 +49,7 @@ export const WithdrawCatalogueJurForm: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
const handleOk = async () => {
|
||||
setConfirmLoading(true)
|
||||
try {
|
||||
const {
|
||||
effective_date,
|
||||
@ -51,8 +57,14 @@ export const WithdrawCatalogueJurForm: React.FC<Props> = ({
|
||||
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<Props> = ({
|
||||
cancelText="Отмена"
|
||||
destroyOnHidden
|
||||
loading={initialDataLoading}
|
||||
confirmLoading={confirmLoading}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
@ -106,13 +119,14 @@ export const WithdrawCatalogueJurForm: React.FC<Props> = ({
|
||||
}
|
||||
>
|
||||
<DatePicker
|
||||
minDate={dayjs()}
|
||||
style={{ width: "100%" }}
|
||||
format="DD.MM.YYYY"
|
||||
placeholder="ДД.ММ.ГГГГ"
|
||||
/>
|
||||
</Form.Item>
|
||||
<FormItem name={"comment"} label="Комментарий">
|
||||
<TextArea maxLength={6} rows={4} placeholder="Необязательно"/>
|
||||
<TextArea maxLength={6} rows={4} placeholder="Необязательно" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user