updated company group and forms

This commit is contained in:
Grog 2026-10-15 16:11:02 +03:00
parent 83d4c2b894
commit 71dae2173d
30 changed files with 1492 additions and 222 deletions

View File

@ -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>
); );
} }

View File

@ -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);
}} }}
/> />
)} )}

View File

@ -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);
}} }}
/> />
)} )}

View File

@ -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,
);
}

View 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}`);

View File

@ -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;

View File

@ -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
} }

View File

@ -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;
}) })

View File

@ -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;

View File

@ -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;
}

View 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";

View 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 }),
};
}

View File

@ -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`), активность — переключателем

View 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;
}

View 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}` },
]}
/>
);
};

View 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>
);
};

View File

@ -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>

View File

@ -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);
}} }}
/> />
)} )}

View File

@ -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;
} },
}, },
], ],
}; };

View 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>
);
};

View File

@ -0,0 +1,4 @@
.ant-listy-item{
border: 0 !important;
padding: 0 !important;
}

View File

@ -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>
); );
}} }}
/> />

View File

@ -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;
}

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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="ЮЛ">

View 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>
);
};

View 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>
);
}

View 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");
},
},
],
};

View File

@ -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>