updated company group and forms
This commit is contained in:
parent
83d4c2b894
commit
71dae2173d
54
src/App.tsx
54
src/App.tsx
@ -1,9 +1,9 @@
|
|||||||
import './App.css';
|
import "./App.css";
|
||||||
import { Layout } from 'antd';
|
import { Layout } from "antd";
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from "react-redux";
|
||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
import { AuthGate } from './auth/AuthGate';
|
import { AuthGate } from "./auth/AuthGate";
|
||||||
import { Config } from './conf/config';
|
import { Config } from "./conf/config";
|
||||||
import {
|
import {
|
||||||
Admin,
|
Admin,
|
||||||
AuditLogging,
|
AuditLogging,
|
||||||
@ -16,38 +16,50 @@ import {
|
|||||||
GroupsLayout,
|
GroupsLayout,
|
||||||
Main,
|
Main,
|
||||||
Mono,
|
Mono,
|
||||||
} from './layouts/';
|
} from "./layouts/";
|
||||||
import { store } from './store/store';
|
import { store } from "./store/store";
|
||||||
import { Sidebar } from './widgets/Sidebar/Sidebar';
|
import { Sidebar } from "./widgets/Sidebar/Sidebar";
|
||||||
|
import { App as AntApp } from "antd";
|
||||||
|
|
||||||
// Каркас: экраны по docs/context/10-user-journey.md добавляются срезами.
|
// Каркас: экраны по docs/context/10-user-journey.md добавляются срезами.
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<AntApp>
|
||||||
<BrowserRouter basename={Config.APP_ADDRESS}>
|
<BrowserRouter basename={Config.APP_ADDRESS}>
|
||||||
<AuthGate>
|
<AuthGate>
|
||||||
<Layout hasSider={true} style={{ height: '100vh' }}>
|
<Layout hasSider={true} style={{ height: "100vh" }}>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Layout.Content>
|
<Layout.Content>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/admin' element={<Admin />} />
|
<Route path="/admin" element={<Admin />} />
|
||||||
<Route path='/' element={<Main />} />
|
<Route path="/" element={<Main />} />
|
||||||
<Route path='/groups'>
|
<Route path="/groups">
|
||||||
<Route index element={<GroupsLayout />} />
|
<Route index element={<GroupsLayout />} />
|
||||||
<Route path=':group_id' element={<ConsolidationPage />} />
|
<Route path=":group_id" element={<ConsolidationPage />} />
|
||||||
<Route path=':group_id/versions/:version_id/osv' element={<ConsolidatedOsvPage />} />
|
<Route
|
||||||
<Route path='monozaemchiki' element={<Mono />} />
|
path=":group_id/versions/:version_id/osv"
|
||||||
|
element={<ConsolidatedOsvPage />}
|
||||||
|
/>
|
||||||
|
<Route path="monozaemchiki" element={<Mono />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path='/catalogue/groups' element={<CatalogueGroup />} />
|
<Route
|
||||||
<Route path='/catalogue/jur_l' element={<CatalogueJur />} />
|
path="/catalogue/groups"
|
||||||
<Route path='/docstorage' element={<DocStorage />} />
|
element={<CatalogueGroup />}
|
||||||
<Route path='/documents/:document_id' element={<DocumentPage />} />
|
/>
|
||||||
<Route path='/logging' element={<AuditLogging />} />
|
<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>
|
</Routes>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</AuthGate>
|
</AuthGate>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</AntApp>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,31 +2,139 @@ import { Button, Space } from "antd";
|
|||||||
import PageTitle from "../components/PageTitle";
|
import PageTitle from "../components/PageTitle";
|
||||||
import { CatalogueJurTable } from "../widgets/CatalogueJurTable/CatalogueJurTable";
|
import { CatalogueJurTable } from "../widgets/CatalogueJurTable/CatalogueJurTable";
|
||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "../store/store";
|
import { useAppDispatch, useAppSelector } from "../store/store";
|
||||||
import { CatalogueJurForm } from "../widgets/CatalogueJurForm/CatalogueJurForm";
|
import { CatalogueJurForm } from "../widgets/CatalogueJurForm/CatalogueJurForm";
|
||||||
import { getCatalogueJurThunk } from "../store/slices/catalogueJurSlice/thunks";
|
import { getCatalogueJurThunk } from "../store/slices/catalogueJurSlice/thunks";
|
||||||
import { CatalogueJurFormProps } from "../types/catalogueJur/catalogueJur";
|
import { CatalogueJurFormProps } from "../types/catalogueJur/catalogueJur";
|
||||||
import { createCatalogueJur } from "../services/api/directories/catalogue_jur";
|
import { createCatalogueJur } from "../services/api/directories/catalogue_jur";
|
||||||
import { CompanyGroupsList } from "../widgets/CompanyGroupsList/CompanyGroupsList";
|
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() {
|
export function CatalogueGroup() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const notification = useNotificationApi();
|
||||||
|
|
||||||
const isGettingArchive = useAppSelector(
|
const isGettingArchive = useAppSelector(
|
||||||
(state) => state.catalogue_jur.isGettingArchive,
|
(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) {
|
async function createJurHandler(values: CatalogueJurFormProps) {
|
||||||
try {
|
try {
|
||||||
await createCatalogueJur(values);
|
await createCatalogueJur(values);
|
||||||
|
dispatch(getCompanyGroupsThunk());
|
||||||
dispatch(
|
dispatch(
|
||||||
getCatalogueJurThunk({ closed: isGettingArchive, outside_group: true }),
|
getCatalogueJurThunk({
|
||||||
|
closed: isGettingArchive,
|
||||||
|
outside_group: false,
|
||||||
|
company_group_id: activeCompanyGroup?.id,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setCreateFormOpen(false);
|
setKindForm(null);
|
||||||
} catch {}
|
} 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -45,7 +153,7 @@ export function CatalogueGroup() {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => setCreateFormOpen(true)}
|
onClick={() => setEntityCreateModalOpen(true)}
|
||||||
>
|
>
|
||||||
Создать ГК/ЮЛ
|
Создать ГК/ЮЛ
|
||||||
</Button>
|
</Button>
|
||||||
@ -59,21 +167,94 @@ export function CatalogueGroup() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ width: 320, flexShrink: 0, minHeight: 0 }}>
|
<div style={{ width: 320, flexShrink: 0, minHeight: 0 }}>
|
||||||
<CompanyGroupsList/>
|
<CompanyGroupsList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1, minWidth: 0, minHeight: 0, borderRadius:12, background:'white' }}>
|
<div
|
||||||
<CatalogueJurTable outside_group={false} />
|
style={{
|
||||||
</div>
|
flex: 1,
|
||||||
</div>
|
minWidth: 0,
|
||||||
{createFormOpen && (
|
minHeight: 0,
|
||||||
<CatalogueJurForm
|
borderRadius: 12,
|
||||||
open={createFormOpen}
|
background: "white",
|
||||||
onCancel={() => {
|
display: "flex",
|
||||||
setCreateFormOpen(false);
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
onOk={(values) => {
|
>
|
||||||
createJurHandler(values);
|
<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>
|
||||||
|
<CreateEntityPickerModal
|
||||||
|
open={entityCreateModalOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setEntityCreateModalOpen(false);
|
||||||
|
}}
|
||||||
|
onSelect={(kind) => {
|
||||||
|
setKindForm(kind);
|
||||||
|
setEntityCreateModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{kindForm === "ul" && (
|
||||||
|
<CatalogueJurForm
|
||||||
|
open={kindForm === "ul"}
|
||||||
|
onCancel={() => {
|
||||||
|
setKindForm(null);
|
||||||
|
}}
|
||||||
|
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 { getCatalogueJurThunk } from "../store/slices/catalogueJurSlice/thunks";
|
||||||
import { CatalogueJurFormProps } from "../types/catalogueJur/catalogueJur";
|
import { CatalogueJurFormProps } from "../types/catalogueJur/catalogueJur";
|
||||||
import { createCatalogueJur } from "../services/api/directories/catalogue_jur";
|
import { createCatalogueJur } from "../services/api/directories/catalogue_jur";
|
||||||
|
import { ApiError } from "../api/client";
|
||||||
|
import { useNotificationApi } from "../utils/useNotification";
|
||||||
|
|
||||||
export function CatalogueJur() {
|
export function CatalogueJur() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const notification = useNotificationApi();
|
||||||
|
|
||||||
const isGettingArchive = useAppSelector(
|
const isGettingArchive = useAppSelector(
|
||||||
(state) => state.catalogue_jur.isGettingArchive,
|
(state) => state.catalogue_jur.isGettingArchive,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [createFormOpen, setCreateFormOpen] = useState(false);
|
const [createFormOpen, setCreateFormOpen] = useState(false);
|
||||||
const outside_group = true
|
const outside_group = true;
|
||||||
async function createJurHandler(values: CatalogueJurFormProps) {
|
async function createJurHandler(values: CatalogueJurFormProps) {
|
||||||
try {
|
try {
|
||||||
await createCatalogueJur(values);
|
await createCatalogueJur(values);
|
||||||
dispatch(
|
dispatch(
|
||||||
getCatalogueJurThunk({ closed: isGettingArchive, outside_group: outside_group }),
|
getCatalogueJurThunk({
|
||||||
|
closed: isGettingArchive,
|
||||||
|
outside_group: outside_group,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
setCreateFormOpen(false);
|
setCreateFormOpen(false);
|
||||||
} catch {}
|
} catch (e) {
|
||||||
|
if (e instanceof ApiError) {
|
||||||
|
notification.error({
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -46,15 +59,15 @@ export function CatalogueJur() {
|
|||||||
Создать ЮЛ
|
Создать ЮЛ
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
<CatalogueJurTable outside_group={outside_group}/>
|
<CatalogueJurTable outside_group={outside_group} />
|
||||||
{createFormOpen && (
|
{createFormOpen && (
|
||||||
<CatalogueJurForm
|
<CatalogueJurForm
|
||||||
open={createFormOpen}
|
open={createFormOpen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setCreateFormOpen(false);
|
setCreateFormOpen(false);
|
||||||
}}
|
}}
|
||||||
onOk={(values) => {
|
onOk={async(values) => {
|
||||||
createJurHandler(values);
|
await createJurHandler(values);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { api } from '../../../api/client';
|
import { api } from '../../../api/client';
|
||||||
import type { LegalEntityBrief, Page, RelatedGroupDetail, RelatedGroupOut } from '../../../types/api';
|
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 CG_PAGE = 200;
|
||||||
const LE_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 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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { CatalogueJurSliceType } from "./types";
|
import { CatalogueJurSliceType } from "./types";
|
||||||
import { getCatalogueJurThunk } from "./thunks";
|
import { getCatalogueJurThunk } from "./thunks";
|
||||||
|
import { RelatedGroupOut } from "../../../types/api";
|
||||||
|
|
||||||
const initialState: CatalogueJurSliceType = {
|
const initialState: CatalogueJurSliceType = {
|
||||||
raw_rows: [],
|
raw_rows: [],
|
||||||
@ -8,6 +9,7 @@ const initialState: CatalogueJurSliceType = {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
isGettingArchive: false,
|
isGettingArchive: false,
|
||||||
|
relatedGroupFilter: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const catalogueJurSlice = createSlice({
|
const catalogueJurSlice = createSlice({
|
||||||
@ -17,6 +19,9 @@ const catalogueJurSlice = createSlice({
|
|||||||
setIsGettingArchive(state, action: PayloadAction<boolean>) {
|
setIsGettingArchive(state, action: PayloadAction<boolean>) {
|
||||||
state.isGettingArchive = action.payload;
|
state.isGettingArchive = action.payload;
|
||||||
},
|
},
|
||||||
|
setRelatedGroupFilter(state, action: PayloadAction<RelatedGroupOut | null>) {
|
||||||
|
state.relatedGroupFilter = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
@ -35,5 +40,5 @@ const catalogueJurSlice = createSlice({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const { setIsGettingArchive } = catalogueJurSlice.actions;
|
export const { setIsGettingArchive, setRelatedGroupFilter } = catalogueJurSlice.actions;
|
||||||
export default catalogueJurSlice.reducer;
|
export default catalogueJurSlice.reducer;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { RelatedGroupOut } from "../../../types/api";
|
||||||
import { ICatalogueJur } from "../../../types/catalogueJur/catalogueJur";
|
import { ICatalogueJur } from "../../../types/catalogueJur/catalogueJur";
|
||||||
|
|
||||||
export interface CatalogueJurSliceType{
|
export interface CatalogueJurSliceType{
|
||||||
@ -5,7 +6,8 @@ export interface CatalogueJurSliceType{
|
|||||||
raw_rows: ICatalogueJur[];
|
raw_rows: ICatalogueJur[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
isGettingArchive:boolean
|
isGettingArchive:boolean;
|
||||||
|
relatedGroupFilter?:RelatedGroupOut | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,12 @@ const comGroupsSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(getCompanyGroupsThunk.fulfilled, (state, action) => {
|
.addCase(getCompanyGroupsThunk.fulfilled, (state, action) => {
|
||||||
state.raw_rows = action.payload;
|
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.total_groups = action.payload.length;
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
})
|
})
|
||||||
|
|||||||
@ -275,6 +275,7 @@ export interface RelatedGroupMemberOut {
|
|||||||
}
|
}
|
||||||
export interface RelatedGroupDetail extends RelatedGroupOut {
|
export interface RelatedGroupDetail extends RelatedGroupOut {
|
||||||
members: RelatedGroupMemberOut[];
|
members: RelatedGroupMemberOut[];
|
||||||
|
crm_id:string
|
||||||
}
|
}
|
||||||
export interface LegalEntityBrief {
|
export interface LegalEntityBrief {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -17,3 +17,9 @@ export const toCompanyGroupRow = (g: ICompanyGroupOut): ICompanyGroup => ({
|
|||||||
...g,
|
...g,
|
||||||
base_regional_office_name: g.base_regional_office?.name ?? '—',
|
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)
|
// Загрузчики — функции уровня модуля: они не меняются между отрисовками (требование useAsyncData)
|
||||||
const loadUsers = () => api.get<Page<AdminUser>>(`/admin/users?limit=${USERS_LIMIT}`);
|
const loadUsers = () => api.get<Page<AdminUser>>(`/admin/users?limit=${USERS_LIMIT}`);
|
||||||
const loadRoles = () => api.get<Role[]>('/admin/roles');
|
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`), активность — переключателем
|
* Таблица пользователей: роли меняются прямо в строке (`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 { Modal, Form, Input, Select, Row, Col, InputNumber } from "antd";
|
||||||
import { CatalogueJurFormProps } from "../../types/catalogueJur/catalogueJur";
|
import { CatalogueJurFormProps } from "../../types/catalogueJur/catalogueJur";
|
||||||
import { useAppDispatch, useAppSelector } from "../../store/store";
|
import { useAppSelector } from "../../store/store";
|
||||||
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
|
|
||||||
import { fetchOneCatalogueJur } from "../../services/api/directories/catalogue_jur";
|
import { fetchOneCatalogueJur } from "../../services/api/directories/catalogue_jur";
|
||||||
import { fetchRelatedGroups } from "../../services/api/directories/company_groups";
|
import { fetchRelatedGroups } from "../../services/api/directories/company_groups";
|
||||||
import { RelatedGroupOut } from "../../types/api";
|
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 = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -22,25 +23,29 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
jurId,
|
jurId,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm<CatalogueJurFormProps>();
|
const [form] = Form.useForm<CatalogueJurFormProps>();
|
||||||
const dispatch = useAppDispatch();
|
const notification = useNotificationApi();
|
||||||
const { me } = useMe();
|
|
||||||
|
|
||||||
const type = Form.useWatch("entity_type", form);
|
|
||||||
const activeCompanyGroup = useAppSelector(
|
|
||||||
(state) => state.company_groups.activeCompanyGroup,
|
|
||||||
);
|
|
||||||
const [initialDataLoading, setInitialDataLoading] = useState(false);
|
const [initialDataLoading, setInitialDataLoading] = useState(false);
|
||||||
const [gskData, setGskData] = useState<RelatedGroupOut[]>([]);
|
const [gskData, setGskData] = useState<RelatedGroupOut[]>([]);
|
||||||
const [gszData, setGszData] = 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() {
|
async function getInitialData() {
|
||||||
setInitialDataLoading(true);
|
|
||||||
form.setFieldsValue({
|
|
||||||
company_group_id: jurId ? undefined : activeCompanyGroup?.id,
|
|
||||||
regional_office_id: me?.regional_office.id,
|
|
||||||
});
|
|
||||||
if (jurId) {
|
if (jurId) {
|
||||||
|
setInitialDataLoading(true);
|
||||||
const data = await fetchOneCatalogueJur(jurId);
|
const data = await fetchOneCatalogueJur(jurId);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: data?.name,
|
name: data?.name,
|
||||||
@ -50,38 +55,57 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
inn: data.inn,
|
inn: data.inn,
|
||||||
kpp: data?.kpp,
|
kpp: data?.kpp,
|
||||||
regional_office_id: data?.regional_office_id,
|
regional_office_id: data?.regional_office_id,
|
||||||
|
company_group_id: data?.company_group_id,
|
||||||
gsk_id: data.memberships?.find((elem) => elem.kind === "gsk")
|
gsk_id: data.memberships?.find((elem) => elem.kind === "gsk")
|
||||||
?.related_group_id,
|
?.related_group_id,
|
||||||
gsz_id: data.memberships?.find((elem) => elem.kind === "gsz")
|
gsz_id: data.memberships?.find((elem) => elem.kind === "gsz")
|
||||||
?.related_group_id,
|
?.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() {
|
async function getRelatedGroupsHandler() {
|
||||||
if (activeCompanyGroup?.id) {
|
if (companyGroupId) {
|
||||||
setLoadingOptions(true);
|
setLoadingRelatedGroupsOptions(true);
|
||||||
const res = await fetchRelatedGroups(activeCompanyGroup?.id);
|
const res = await fetchRelatedGroups(companyGroupId);
|
||||||
const gsks = res.filter((elem) => elem.kind === "gsk");
|
const gsks = res.filter((elem) => elem.kind === "gsk");
|
||||||
const gszs = res.filter((elem) => elem.kind === "gsz");
|
const gszs = res.filter((elem) => elem.kind === "gsz");
|
||||||
setGskData(gsks);
|
setGskData(gsks);
|
||||||
setGszData(gszs);
|
setGszData(gszs);
|
||||||
setLoadingOptions(false);
|
setLoadingRelatedGroupsOptions(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
await onOk({
|
await onOk({
|
||||||
...values,
|
...values,
|
||||||
kpp: type === "ИП" ? undefined : values.kpp?.toString(),
|
kpp: type === "ИП" ? undefined : values.kpp?.toString(),
|
||||||
inn: jurId ? undefined : values?.inn?.toString(),
|
inn: jurId ? undefined : values?.inn?.toString(),
|
||||||
|
company_group_id: jurId ? undefined : values.company_group_id,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (e) {
|
||||||
return;
|
if (e instanceof ApiError) {
|
||||||
|
notification.error({
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,19 +114,22 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const companyGroupOptions = useMemo(() => {
|
||||||
if (open) {
|
return companyGrops?.map(
|
||||||
dispatch(getCompanyGroupsThunk());
|
(company) => company && { value: company.id, label: company.name },
|
||||||
}
|
);
|
||||||
}, [open, dispatch]);
|
}, [companyGrops]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getInitialData();
|
getInitialData();
|
||||||
}, [open]);
|
}, [jurId, open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getOfficesOptionsHandler();
|
||||||
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getRelatedGroupsHandler();
|
getRelatedGroupsHandler();
|
||||||
}, [activeCompanyGroup]);
|
}, [companyGroupId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -116,6 +143,7 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
cancelText="Отмена"
|
cancelText="Отмена"
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
width={720}
|
width={720}
|
||||||
|
confirmLoading={confirmLoading}
|
||||||
loading={initialDataLoading}
|
loading={initialDataLoading}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
@ -124,16 +152,23 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="inn"
|
name="inn"
|
||||||
label="ИНН"
|
label="ИНН"
|
||||||
rules={[
|
rules={
|
||||||
|
type === "ИП"
|
||||||
|
? [
|
||||||
|
{ required: true, message: "Укажите ИНН" },
|
||||||
|
{ pattern: /^\d{12}$/, message: "12 цифр" },
|
||||||
|
]
|
||||||
|
: [
|
||||||
{ required: true, message: "Укажите ИНН" },
|
{ required: true, message: "Укажите ИНН" },
|
||||||
{ pattern: /^\d{10}$/, message: "10 цифр" },
|
{ pattern: /^\d{10}$/, message: "10 цифр" },
|
||||||
]}
|
]
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
disabled={!!jurId}
|
disabled={!!jurId}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
placeholder="0000000000"
|
placeholder={type === "ИП" ? "000000000000" : "0000000000"}
|
||||||
maxLength={10}
|
maxLength={type === "ИП" ? 12 : 10}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
@ -155,10 +190,7 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="crm_id"
|
name="crm_id"
|
||||||
label="ID ЮЛ в CRM"
|
label="ID ЮЛ в CRM"
|
||||||
rules={[
|
rules={[{ len: 11, message: "Должно быть 11 символов" }]}
|
||||||
{ required: true, message: "Укажите CRM ID" },
|
|
||||||
{ pattern: /^\d{11}$/, message: "11 цифр" },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input maxLength={11} />
|
<Input maxLength={11} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -171,6 +203,7 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
disabled={!!jurId}
|
||||||
options={[
|
options={[
|
||||||
{ value: "ЮЛ", label: "ЮЛ" },
|
{ value: "ЮЛ", label: "ЮЛ" },
|
||||||
{ value: "ИП", label: "ИП" },
|
{ value: "ИП", label: "ИП" },
|
||||||
@ -203,6 +236,38 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
notFoundContent={null}
|
notFoundContent={null}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
@ -213,7 +278,9 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
options={gskData?.map((g) => ({ value: g.id, label: g.name }))}
|
options={gskData?.map((g) => ({ value: g.id, label: g.name }))}
|
||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"}
|
notFoundContent={
|
||||||
|
loadingRelatedGroupsOptions ? "Загрузка" : "Нет вариантов"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
@ -228,7 +295,9 @@ export const CatalogueJurForm: React.FC<Props> = ({
|
|||||||
}))}
|
}))}
|
||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"}
|
notFoundContent={
|
||||||
|
loadingRelatedGroupsOptions ? "Загрузка" : "Нет вариантов"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</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 { Button, Checkbox, Dropdown, Input, Space, Tabs } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import type { ColumnsType } from "antd/es/table";
|
import type { ColumnsType } from "antd/es/table";
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from "../../components/GenericTable";
|
} from "../../components/GenericTable";
|
||||||
import { catalogueJurConfig } from "./config";
|
import { catalogueJurConfig } from "./config";
|
||||||
import {
|
import {
|
||||||
|
CloseOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
EllipsisOutlined,
|
EllipsisOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
@ -33,21 +34,28 @@ import {
|
|||||||
withdrawCatalogueJur,
|
withdrawCatalogueJur,
|
||||||
} from "../../services/api/directories/catalogue_jur";
|
} from "../../services/api/directories/catalogue_jur";
|
||||||
import { WithdrawCatalogueJurForm } from "../WithdrawCatalogueJurForm/WithdrawCatalogueJurForm";
|
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 { CatalogueJurForm } from "../CatalogueJurForm/CatalogueJurForm";
|
||||||
import Title from "antd/es/typography/Title";
|
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
|
||||||
import Text from "antd/es/typography/Text";
|
import { ApiError } from "../../api/client";
|
||||||
import { fetchRelatedGroups } from "../../services/api/directories/company_groups";
|
import { useNotificationApi } from "../../utils/useNotification";
|
||||||
import { RelatedGroupOut } from "../../types/api";
|
|
||||||
|
|
||||||
interface CatalogueJurTableI {
|
interface CatalogueJurTableI {
|
||||||
outside_group: boolean;
|
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 navigate = useNavigate();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const notification = useNotificationApi();
|
||||||
|
|
||||||
const rawRows = useAppSelector((state) => state.catalogue_jur.raw_rows);
|
const rawRows = useAppSelector((state) => state.catalogue_jur.raw_rows);
|
||||||
const loading = useAppSelector((state) => state.catalogue_jur.loading);
|
const loading = useAppSelector((state) => state.catalogue_jur.loading);
|
||||||
const isGettingArchive = useAppSelector(
|
const isGettingArchive = useAppSelector(
|
||||||
@ -57,14 +65,27 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
(state) => state.company_groups.activeCompanyGroup,
|
(state) => state.company_groups.activeCompanyGroup,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const relatedGroupFilter = useAppSelector(
|
||||||
|
(state) => state.catalogue_jur.relatedGroupFilter,
|
||||||
|
);
|
||||||
const [moveCatalogueFormOpen, setMoveCatalogueFormOpen] = useState(false);
|
const [moveCatalogueFormOpen, setMoveCatalogueFormOpen] = useState(false);
|
||||||
const [editCatalogueFormOpen, setEditCatalogueFormOpen] = useState(false);
|
const [editCatalogueFormOpen, setEditCatalogueFormOpen] = useState(false);
|
||||||
const [withdrawCatalogueFormOpen, setWithdrawCatalogueFormOpen] =
|
const [withdrawCatalogueFormOpen, setWithdrawCatalogueFormOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>("legal_entities");
|
|
||||||
const [jurId, setJurId] = useState("");
|
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 { visibleKeys, setVisibleKeys } = useVisibleColumns(catalogueJurConfig);
|
||||||
const {
|
const {
|
||||||
rawQuery,
|
rawQuery,
|
||||||
@ -78,7 +99,7 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
} = useTableFilters<ICatalogueJur>();
|
} = useTableFilters<ICatalogueJur>();
|
||||||
// single filtered array — title count and table share it
|
// single filtered array — title count and table share it
|
||||||
const { rows } = useFilteredRows(
|
const { rows } = useFilteredRows(
|
||||||
rawRows,
|
filterdData,
|
||||||
catalogueJurConfig,
|
catalogueJurConfig,
|
||||||
rawQuery,
|
rawQuery,
|
||||||
columnFilters,
|
columnFilters,
|
||||||
@ -90,7 +111,7 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
getCatalogueJurThunk({
|
getCatalogueJurThunk({
|
||||||
closed: isGettingArchive,
|
closed: isGettingArchive,
|
||||||
outside_group: outside_group,
|
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 {
|
try {
|
||||||
await moveCatalogueJur(jurId, values);
|
await moveCatalogueJur(jurId, values);
|
||||||
getCatalogueJur();
|
getCatalogueJur();
|
||||||
|
dispatch(getCompanyGroupsThunk());
|
||||||
closeMoveCatalogueJurHandler();
|
closeMoveCatalogueJurHandler();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
if (e instanceof ApiError) {
|
||||||
|
notification.error({
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function withdrawCatalogueJurHandler(
|
async function withdrawCatalogueJurHandler(
|
||||||
@ -123,39 +149,37 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
try {
|
try {
|
||||||
await withdrawCatalogueJur(jurId, values);
|
await withdrawCatalogueJur(jurId, values);
|
||||||
getCatalogueJur();
|
getCatalogueJur();
|
||||||
|
dispatch(getCompanyGroupsThunk());
|
||||||
|
|
||||||
closeWithdrawCatalogueJurHandler();
|
closeWithdrawCatalogueJurHandler();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
if (e instanceof ApiError) {
|
||||||
|
notification.error({
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editJurHandler(values: CatalogueJurFormProps) {
|
async function editJurHandler(values: CatalogueJurFormProps) {
|
||||||
try {
|
try {
|
||||||
await editCatalogueJur(jurId, values);
|
await editCatalogueJur(jurId, values);
|
||||||
dispatch(
|
|
||||||
getCatalogueJurThunk({ closed: isGettingArchive, outside_group: true }),
|
|
||||||
);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCatalogueJur();
|
getCatalogueJur();
|
||||||
}, [isGettingArchive, activeCompanyGroup]);
|
closeEditCatalogueJurHandler();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ApiError) {
|
||||||
|
notification.error({
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getRelatedGroupsHandler();
|
if (!!activeCompanyGroup?.id) {
|
||||||
}, [activeCompanyGroup]);
|
getCatalogueJur();
|
||||||
|
}
|
||||||
|
}, [isGettingArchive, activeCompanyGroup]);
|
||||||
|
|
||||||
const extraColumns: ColumnsType<ICatalogueJur> = useMemo(
|
const extraColumns: ColumnsType<ICatalogueJur> = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -224,22 +248,9 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
borderRadius: 12,
|
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
|
<Space
|
||||||
style={{
|
style={{
|
||||||
padding: "0 12px",
|
padding: `${outside_group ? "12px" : "0px"} 12px`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
borderBottom: "1px",
|
borderBottom: "1px",
|
||||||
@ -255,19 +266,7 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
style={{ maxWidth: 320 }}
|
style={{ maxWidth: 320 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Tabs
|
tabsComponent
|
||||||
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}` },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Space>
|
<Space>
|
||||||
@ -281,14 +280,12 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
Сбросить фильтры
|
Сбросить фильтры
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{activeTab === "legal_entities" && (
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isGettingArchive}
|
checked={isGettingArchive}
|
||||||
onChange={(e) => dispatch(setIsGettingArchive(e.target.checked))}
|
onChange={(e) => dispatch(setIsGettingArchive(e.target.checked))}
|
||||||
>
|
>
|
||||||
Архив
|
Архив
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
)}
|
|
||||||
<ColumnSettings
|
<ColumnSettings
|
||||||
defs={catalogueJurConfig.columns}
|
defs={catalogueJurConfig.columns}
|
||||||
visibleKeys={visibleKeys}
|
visibleKeys={visibleKeys}
|
||||||
@ -296,6 +293,57 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</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
|
<DataTable
|
||||||
sortState={sort}
|
sortState={sort}
|
||||||
onSortChange={setSort}
|
onSortChange={setSort}
|
||||||
@ -316,8 +364,8 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
closeMoveCatalogueJurHandler();
|
closeMoveCatalogueJurHandler();
|
||||||
}}
|
}}
|
||||||
onOk={(values) => {
|
onOk={async (values) => {
|
||||||
moveCatalogueJurHandler(values);
|
await moveCatalogueJurHandler(values);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -328,8 +376,8 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
closeWithdrawCatalogueJurHandler();
|
closeWithdrawCatalogueJurHandler();
|
||||||
}}
|
}}
|
||||||
onOk={(values) => {
|
onOk={async (values) => {
|
||||||
withdrawCatalogueJurHandler(values);
|
await withdrawCatalogueJurHandler(values);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -340,8 +388,8 @@ export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
closeEditCatalogueJurHandler();
|
closeEditCatalogueJurHandler();
|
||||||
}}
|
}}
|
||||||
onOk={(values) => {
|
onOk={async (values) => {
|
||||||
editJurHandler(values);
|
await editJurHandler(values);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import type { TableConfig } from "../../components/GenericTable";
|
import type { TableConfig } from "../../components/GenericTable";
|
||||||
import { ICatalogueJur } from "../../types/catalogueJur/catalogueJur";
|
import { ICatalogueJur } from "../../types/catalogueJur/catalogueJur";
|
||||||
|
import { TableTag } from "../../components/TableTag/TableTag";
|
||||||
|
import { TAG_PRESETS } from "../../components/TableTag/presets";
|
||||||
|
|
||||||
export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
|
export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
|
||||||
rowKey: "id",
|
rowKey: "id",
|
||||||
@ -10,10 +12,15 @@ export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
|
|||||||
key: "closed",
|
key: "closed",
|
||||||
title: "Статус",
|
title: "Статус",
|
||||||
features: { sort: true },
|
features: { sort: true },
|
||||||
render: (_:unknown, row) =>{
|
render: (_: unknown, row) => {
|
||||||
// #TODO поменять логику если запись не активна
|
// #TODO поменять логику если запись не активна
|
||||||
return <Tag variant="outlined" color={'success'}>{!row.closed ? 'Активно' : 'Не активно'}</Tag>
|
return (
|
||||||
}
|
<TableTag
|
||||||
|
text={!row.closed ? "Активно" : "Не активно"}
|
||||||
|
{...TAG_PRESETS.green}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "inn",
|
key: "inn",
|
||||||
@ -34,16 +41,17 @@ export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
|
|||||||
key: "entity_type",
|
key: "entity_type",
|
||||||
title: "Тип",
|
title: "Тип",
|
||||||
features: { multiselect: true, sort: true },
|
features: { multiselect: true, sort: true },
|
||||||
render: (val) =>{
|
render: (val) => {
|
||||||
return <Tag variant="outlined" color={val === 'ЮЛ' ? 'purple' : 'orange'}>{val as string}</Tag>
|
const preset = val === "ЮЛ" ? TAG_PRESETS.orange : TAG_PRESETS.purple;
|
||||||
}
|
return <TableTag text={val as string} {...preset} />;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "regional_office",
|
key: "regional_office",
|
||||||
title: "Основной РФ",
|
title: "Основной РФ",
|
||||||
render: (val:any) =>{
|
render: (val: any) => {
|
||||||
return val?.name
|
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 { 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 { FolderOpenOutlined, SearchOutlined } from "@ant-design/icons";
|
||||||
import { useAppDispatch, useAppSelector } from "../../store/store";
|
import { useAppDispatch, useAppSelector } from "../../store/store";
|
||||||
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
|
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
|
||||||
import { setActiveCompanyGroup } from "../../store/slices/companyGroupsSlice/companyGroupsSlice";
|
import { setActiveCompanyGroup } from "../../store/slices/companyGroupsSlice/companyGroupsSlice";
|
||||||
|
import "./CompanyGroupsList.css"
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export const CompanyGroupsList = () => {
|
export const CompanyGroupsList = () => {
|
||||||
@ -31,12 +31,6 @@ export const CompanyGroupsList = () => {
|
|||||||
dispatch(getCompanyGroupsThunk());
|
dispatch(getCompanyGroupsThunk());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!activeCompanyGroup && rawRows?.length) {
|
|
||||||
dispatch(setActiveCompanyGroup(rawRows?.[0]));
|
|
||||||
}
|
|
||||||
}, [rawRows, activeCompanyGroup]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -48,11 +42,13 @@ export const CompanyGroupsList = () => {
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ padding: "12px 16px", borderBottom:"1px solid #0f172a0f" }}>
|
<div
|
||||||
|
style={{ padding: "12px 16px", borderBottom: "1px solid #0f172a0f" }}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
allowClear
|
allowClear
|
||||||
prefix={<SearchOutlined style={{ color: "#bfbfbf" }} />}
|
prefix={<SearchOutlined style={{ color: "#bfbfbf" }} />}
|
||||||
placeholder={"Поиск по ЮЛ или ГСК"}
|
placeholder={"Поиск по ГК или CRM ID"}
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -70,13 +66,15 @@ export const CompanyGroupsList = () => {
|
|||||||
style={{ marginTop: 32 }}
|
style={{ marginTop: 32 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<List
|
<Listy
|
||||||
dataSource={filtered}
|
virtual
|
||||||
split={false}
|
rowKey={"id"}
|
||||||
renderItem={(item) => {
|
items={filtered}
|
||||||
|
height={900}
|
||||||
|
itemRender={(item) => {
|
||||||
const active = item.id === activeCompanyGroup?.id;
|
const active = item.id === activeCompanyGroup?.id;
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<div
|
||||||
onClick={() => dispatch(setActiveCompanyGroup(item))}
|
onClick={() => dispatch(setActiveCompanyGroup(item))}
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
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 #2ea64b3d, inset 3px 0 0 #258141"
|
||||||
: "inset 0 0 0 1px #0f172a14",
|
: "inset 0 0 0 1px #0f172a14",
|
||||||
transition: "background 0.15s",
|
transition: "background 0.15s",
|
||||||
|
borderBottom:0
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
@ -175,7 +174,7 @@ export const CompanyGroupsList = () => {
|
|||||||
{item.legal_entities_count}
|
{item.legal_entities_count}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</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 { fetchOneCatalogueJur } from "../../services/api/directories/catalogue_jur";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { MoveCatalogueJurProps } from "../../types/catalogueJur/catalogueJur";
|
import { MoveCatalogueJurProps } from "../../types/catalogueJur/catalogueJur";
|
||||||
|
import { ApiError } from "../../api/client";
|
||||||
|
import { useNotificationApi } from "../../utils/useNotification";
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -30,9 +32,11 @@ export const MoveCatalogueJurForm: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm<FormValues>();
|
const [form] = Form.useForm<FormValues>();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const notification = useNotificationApi();
|
||||||
|
|
||||||
const { raw_rows, loading } = useAppSelector((state) => state.company_groups);
|
const { raw_rows, loading } = useAppSelector((state) => state.company_groups);
|
||||||
const [initialDataLoading, setInitialDataLoading] = useState(false);
|
const [initialDataLoading, setInitialDataLoading] = useState(false);
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
|
||||||
async function getInitialData() {
|
async function getInitialData() {
|
||||||
setInitialDataLoading(true);
|
setInitialDataLoading(true);
|
||||||
@ -48,11 +52,18 @@ export const MoveCatalogueJurForm: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
try {
|
try {
|
||||||
const { effective_date, company_group_id } = await form.validateFields();
|
const { effective_date, company_group_id } = await form.validateFields();
|
||||||
await onOk({ effective_date, company_group_id });
|
await onOk({ effective_date, company_group_id });
|
||||||
} catch {
|
} catch (e) {
|
||||||
// validation error - keep modal open
|
if (e instanceof ApiError) {
|
||||||
|
notification.error({
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,10 +87,11 @@ export const MoveCatalogueJurForm: React.FC<Props> = ({
|
|||||||
open={open}
|
open={open}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
okText="+ Создать"
|
okText="Перевести"
|
||||||
cancelText="Отмена"
|
cancelText="Отмена"
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
loading={initialDataLoading}
|
loading={initialDataLoading}
|
||||||
|
confirmLoading={confirmLoading}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<FormItem name="name" label="ЮЛ">
|
<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 dayjs from "dayjs";
|
||||||
import { WithdrawCatalogueJurProps } from "../../types/catalogueJur/catalogueJur";
|
import { WithdrawCatalogueJurProps } from "../../types/catalogueJur/catalogueJur";
|
||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
|
import { ApiError } from "../../api/client";
|
||||||
|
import { useNotificationApi } from "../../utils/useNotification";
|
||||||
|
|
||||||
interface FormValues extends WithdrawCatalogueJurProps {
|
interface FormValues extends WithdrawCatalogueJurProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -24,6 +26,9 @@ export const WithdrawCatalogueJurForm: React.FC<Props> = ({
|
|||||||
jurId,
|
jurId,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm<FormValues>();
|
const [form] = Form.useForm<FormValues>();
|
||||||
|
const notification = useNotificationApi();
|
||||||
|
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
const [initialDataLoading, setInitialDataLoading] = useState(false);
|
const [initialDataLoading, setInitialDataLoading] = useState(false);
|
||||||
|
|
||||||
const eventOptions = [
|
const eventOptions = [
|
||||||
@ -44,6 +49,7 @@ export const WithdrawCatalogueJurForm: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
|
setConfirmLoading(true)
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
effective_date,
|
effective_date,
|
||||||
@ -51,8 +57,14 @@ export const WithdrawCatalogueJurForm: React.FC<Props> = ({
|
|||||||
comment = "",
|
comment = "",
|
||||||
} = await form.validateFields();
|
} = await form.validateFields();
|
||||||
await onOk({ effective_date, event, comment });
|
await onOk({ effective_date, event, comment });
|
||||||
} catch {
|
} catch (e) {
|
||||||
// validation error - keep modal open
|
if (e instanceof ApiError) {
|
||||||
|
notification.error({
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally{
|
||||||
|
setConfirmLoading(false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,6 +87,7 @@ export const WithdrawCatalogueJurForm: React.FC<Props> = ({
|
|||||||
cancelText="Отмена"
|
cancelText="Отмена"
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
loading={initialDataLoading}
|
loading={initialDataLoading}
|
||||||
|
confirmLoading={confirmLoading}
|
||||||
okButtonProps={{ danger: true }}
|
okButtonProps={{ danger: true }}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
@ -106,13 +119,14 @@ export const WithdrawCatalogueJurForm: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
minDate={dayjs()}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
format="DD.MM.YYYY"
|
format="DD.MM.YYYY"
|
||||||
placeholder="ДД.ММ.ГГГГ"
|
placeholder="ДД.ММ.ГГГГ"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<FormItem name={"comment"} label="Комментарий">
|
<FormItem name={"comment"} label="Комментарий">
|
||||||
<TextArea maxLength={6} rows={4} placeholder="Необязательно"/>
|
<TextArea maxLength={6} rows={4} placeholder="Необязательно" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user