added company group table #31

Merged
aep merged 13 commits from feat/legalEntities into test 2026-10-19 10:35:19 +03:00
11 changed files with 464 additions and 101 deletions
Showing only changes of commit 83d4c2b894 - Show all commits

View File

@ -1,3 +1,82 @@
import { Button, Space } from "antd";
import PageTitle from "../components/PageTitle";
import { CatalogueJurTable } from "../widgets/CatalogueJurTable/CatalogueJurTable";
import { PlusOutlined } from "@ant-design/icons";
import { useState } from "react";
import { useAppDispatch, useAppSelector } from "../store/store";
import { CatalogueJurForm } from "../widgets/CatalogueJurForm/CatalogueJurForm";
import { getCatalogueJurThunk } from "../store/slices/catalogueJurSlice/thunks";
import { CatalogueJurFormProps } from "../types/catalogueJur/catalogueJur";
import { createCatalogueJur } from "../services/api/directories/catalogue_jur";
import { CompanyGroupsList } from "../widgets/CompanyGroupsList/CompanyGroupsList";
export function CatalogueGroup() {
return <h3>ГК и организации</h3>;
const dispatch = useAppDispatch();
const isGettingArchive = useAppSelector(
(state) => state.catalogue_jur.isGettingArchive,
);
const [createFormOpen, setCreateFormOpen] = useState(false);
async function createJurHandler(values: CatalogueJurFormProps) {
try {
await createCatalogueJur(values);
dispatch(
getCatalogueJurThunk({ closed: isGettingArchive, outside_group: true }),
);
setCreateFormOpen(false);
} catch {}
}
return (
<div
style={{
padding: 12,
display: "flex",
flexDirection: "column",
height: "100vh",
}}
>
<Space style={{ justifyContent: "space-between" }}>
<PageTitle
header="Группы компаний и организаций"
location="Справочники"
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setCreateFormOpen(true)}
>
Создать ГК/ЮЛ
</Button>
</Space>
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
gap: 12,
}}
>
<div style={{ width: 320, flexShrink: 0, minHeight: 0 }}>
<CompanyGroupsList/>
</div>
<div style={{ flex: 1, minWidth: 0, minHeight: 0, borderRadius:12, background:'white' }}>
<CatalogueJurTable outside_group={false} />
</div>
</div>
{createFormOpen && (
<CatalogueJurForm
open={createFormOpen}
onCancel={() => {
setCreateFormOpen(false);
}}
onOk={(values) => {
createJurHandler(values);
}}
/>
)}
</div>
);
}

View File

@ -14,14 +14,14 @@ export function CatalogueJur() {
const isGettingArchive = useAppSelector(
(state) => state.catalogue_jur.isGettingArchive,
);
const [createFormOpen, setCreateFormOpen] = useState(false);
const outside_group = true
async function createJurHandler(values: CatalogueJurFormProps) {
try {
await createCatalogueJur(values);
dispatch(
getCatalogueJurThunk({ closed: isGettingArchive, outside_group: true }),
getCatalogueJurThunk({ closed: isGettingArchive, outside_group: outside_group }),
);
setCreateFormOpen(false);
} catch {}
@ -46,17 +46,7 @@ export function CatalogueJur() {
Создать ЮЛ
</Button>
</Space>
<main
style={{
flex: "1 1 auto",
minHeight: 0,
overflow: "auto",
backgroundColor: "white",
borderRadius: 12,
}}
>
<CatalogueJurTable />
</main>
<CatalogueJurTable outside_group={outside_group}/>
{createFormOpen && (
<CatalogueJurForm
open={createFormOpen}

View File

@ -12,4 +12,5 @@ export interface CatalogueJurSliceType{
export interface GetCatalogueJurPropsI{
closed?:boolean
outside_group?:boolean
company_group_id?:string
}

View File

@ -1,34 +1,40 @@
import { createSlice } from '@reduxjs/toolkit';
import { getCompanyGroupsThunk } from './thunks';
import type { CompanyGroupSliceType } from './types';
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { getCompanyGroupsThunk } from "./thunks";
import type { CompanyGroupSliceType } from "./types";
import { ICompanyGroup } from "../../../types/companyGroups/companyGroups";
const initialState: CompanyGroupSliceType = {
raw_rows: [],
total_groups: 0,
loading: false,
error: null,
raw_rows: [],
total_groups: 0,
loading: false,
error: null,
activeCompanyGroup: null,
};
const comGroupsSlice = createSlice({
name: 'company_groups',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getCompanyGroupsThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getCompanyGroupsThunk.fulfilled, (state, action) => {
state.raw_rows = action.payload;
state.total_groups = action.payload.length;
state.loading = false;
})
.addCase(getCompanyGroupsThunk.rejected, (state, action) => {
state.error = action.payload ?? 'Неизвестная ошибка';
state.loading = false;
});
},
name: "company_groups",
initialState,
reducers: {
setActiveCompanyGroup(state, action: PayloadAction<ICompanyGroup | null>) {
state.activeCompanyGroup = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(getCompanyGroupsThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getCompanyGroupsThunk.fulfilled, (state, action) => {
state.raw_rows = action.payload;
state.total_groups = action.payload.length;
state.loading = false;
})
.addCase(getCompanyGroupsThunk.rejected, (state, action) => {
state.error = action.payload ?? "Неизвестная ошибка";
state.loading = false;
});
},
});
export const { setActiveCompanyGroup } = comGroupsSlice.actions;
export default comGroupsSlice.reducer;

View File

@ -5,4 +5,5 @@ export interface CompanyGroupSliceType {
raw_rows: ICompanyGroup[];
loading: boolean;
error: string | null;
activeCompanyGroup?:ICompanyGroup | null
}

View File

@ -14,7 +14,7 @@ export interface ICatalogueJur {
updated_at: Date;
outside_group: boolean;
closed: boolean | string;
memberships?: unknown[];
memberships?: MemberShips[];
aliases?: JurAlliase[];
regional_office: {
id: string;
@ -23,6 +23,16 @@ export interface ICatalogueJur {
};
}
export type KindType = "gsk" | "gsz"
export type MemberShips = {
related_group_id: string;
code: string;
kind: KindType;
name: string;
valid_from: Date;
};
export type JurAlliase = {
alias: string;
created_at: Date;
@ -46,7 +56,9 @@ export type CatalogueJurFormProps = {
crm_id: string;
entity_type: "ЮЛ" | "ИП";
name: string;
aliases?: string[] | {value:string, label:string}[];
aliases?: string[] | { value: string; label: string }[];
company_group_id?: string;
regional_office_id?: string;
gsk_id?: string;
gsz_id?: string;
};

View File

@ -1,12 +1,12 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { Modal, Form, Input, Select, Row, Col, InputNumber } from "antd";
import { CatalogueJurFormProps } from "../../types/catalogueJur/catalogueJur";
import { useAppDispatch, useAppSelector } from "../../store/store";
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
import { api } from "../../api/client";
import { RegionalOffice } from "../../api/types";
import { useAsyncData } from "../Admin/hooks";
import { fetchOneCatalogueJur } from "../../services/api/directories/catalogue_jur";
import { fetchRelatedGroups } from "../../services/api/directories/company_groups";
import { RelatedGroupOut } from "../../types/api";
import { useMe } from "../../auth/useMe";
type Props = {
open: boolean;
@ -23,34 +23,55 @@ export const CatalogueJurForm: React.FC<Props> = ({
}) => {
const [form] = Form.useForm<CatalogueJurFormProps>();
const dispatch = useAppDispatch();
const loadOffices = useCallback(
() => api.get<RegionalOffice[]>("/directories/regional-offices"),
[],
);
const offices = useAsyncData(loadOffices);
const type = Form.useWatch("entity_type", form);
const { me } = useMe();
const { raw_rows, loading } = useAppSelector((state) => state.company_groups);
const type = Form.useWatch("entity_type", form);
const activeCompanyGroup = useAppSelector(
(state) => state.company_groups.activeCompanyGroup,
);
const [initialDataLoading, setInitialDataLoading] = useState(false);
const [gskData, setGskData] = useState<RelatedGroupOut[]>([]);
const [gszData, setGszData] = useState<RelatedGroupOut[]>([]);
const [loadingOptions, setLoadingOptions] = useState(false);
async function getInitialData() {
setInitialDataLoading(true);
form.setFieldsValue({
company_group_id: jurId ? undefined : activeCompanyGroup?.id,
regional_office_id: me?.regional_office.id,
});
if (jurId) {
const data = await fetchOneCatalogueJur(jurId);
form.setFieldsValue({
name: data?.name,
company_group_id: data?.company_group_id,
aliases: data?.aliases?.map((aliase) => aliase.alias),
crm_id: data?.crm_id,
entity_type: data?.entity_type,
inn: data.inn,
kpp: data?.kpp,
regional_office_id: data?.regional_office_id,
gsk_id: data.memberships?.find((elem) => elem.kind === "gsk")
?.related_group_id,
gsz_id: data.memberships?.find((elem) => elem.kind === "gsz")
?.related_group_id,
});
}
setInitialDataLoading(false);
}
async function getRelatedGroupsHandler() {
if (activeCompanyGroup?.id) {
setLoadingOptions(true);
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);
setLoadingOptions(false);
}
}
const handleOk = async () => {
try {
const values = await form.validateFields();
@ -58,7 +79,6 @@ export const CatalogueJurForm: React.FC<Props> = ({
...values,
kpp: type === "ИП" ? undefined : values.kpp?.toString(),
inn: jurId ? undefined : values?.inn?.toString(),
company_group_id: jurId ? undefined : values.company_group_id,
});
} catch {
return;
@ -71,16 +91,19 @@ export const CatalogueJurForm: React.FC<Props> = ({
};
useEffect(() => {
if (open && raw_rows.length === 0 && !loading) {
if (open) {
dispatch(getCompanyGroupsThunk());
}
}, [open, raw_rows.length, loading, dispatch]);
}, [open, dispatch]);
useEffect(() => {
if (!!jurId) {
getInitialData();
}
getInitialData();
}, [open]);
useEffect(() => {
getRelatedGroupsHandler();
}, [activeCompanyGroup]);
return (
<Modal
title={
@ -183,30 +206,29 @@ export const CatalogueJurForm: React.FC<Props> = ({
<Row gutter={16}>
<Col span={12}>
<Form.Item name="company_group_id" label="Входит в ГСК">
<Form.Item name="gsk_id" label="Входит в ГСК">
<Select
disabled={!!jurId}
allowClear
placeholder="— не указан —"
options={raw_rows?.map((g) => ({ value: g.id, label: g.name }))}
options={gskData?.map((g) => ({ value: g.id, label: g.name }))}
showSearch
optionFilterProp="label"
notFoundContent={loading ? "Загрузка" : "Нет вариантов"}
notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="regional_office_id" label="Входит в ГСЗ">
<Form.Item name="gsz_id" label="Входит в ГСЗ">
<Select
allowClear
placeholder="— не указан —"
options={offices?.data?.map((g) => ({
options={gszData?.map((g) => ({
value: g.id,
label: g.name,
}))}
showSearch
optionFilterProp="label"
notFoundContent={loading ? "Загрузка" : "Нет вариантов"}
notFoundContent={loadingOptions ? "Загрузка" : "Нет вариантов"}
/>
</Form.Item>
</Col>

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Checkbox, Dropdown, Input, Space } from "antd";
import { Button, Checkbox, Dropdown, Input, Space, Tabs } from "antd";
import { useNavigate } from "react-router-dom";
import type { ColumnsType } from "antd/es/table";
import {
@ -35,8 +35,17 @@ import {
import { WithdrawCatalogueJurForm } from "../WithdrawCatalogueJurForm/WithdrawCatalogueJurForm";
import { setIsGettingArchive } from "../../store/slices/catalogueJurSlice/catalogueJurSlice";
import { CatalogueJurForm } from "../CatalogueJurForm/CatalogueJurForm";
import Title from "antd/es/typography/Title";
import Text from "antd/es/typography/Text";
import { fetchRelatedGroups } from "../../services/api/directories/company_groups";
import { RelatedGroupOut } from "../../types/api";
export function CatalogueJurTable() {
interface CatalogueJurTableI {
outside_group: boolean;
}
type TabKey = "legal_entities" | "gsk" | "gsz";
export function CatalogueJurTable({ outside_group }: CatalogueJurTableI) {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const rawRows = useAppSelector((state) => state.catalogue_jur.raw_rows);
@ -44,12 +53,18 @@ export function CatalogueJurTable() {
const isGettingArchive = useAppSelector(
(state) => state.catalogue_jur.isGettingArchive,
);
const activeCompanyGroup = useAppSelector(
(state) => state.company_groups.activeCompanyGroup,
);
const [moveCatalogueFormOpen, setMoveCatalogueFormOpen] = useState(false);
const [editCatalogueFormOpen, setEditCatalogueFormOpen] = useState(false);
const [withdrawCatalogueFormOpen, setWithdrawCatalogueFormOpen] =
useState(false);
const [activeTab, setActiveTab] = useState<TabKey>("legal_entities");
const [jurId, setJurId] = useState("");
const [gskData, setGskData] = useState<RelatedGroupOut[]>([]);
const [gszData, setGszData] = useState<RelatedGroupOut[]>([]);
const { visibleKeys, setVisibleKeys } = useVisibleColumns(catalogueJurConfig);
const {
rawQuery,
@ -71,9 +86,12 @@ export function CatalogueJurTable() {
);
function getCatalogueJur() {
// #TODO поменять на outside_group=true
dispatch(
getCatalogueJurThunk({ closed: isGettingArchive, outside_group: true }),
getCatalogueJurThunk({
closed: isGettingArchive,
outside_group: outside_group,
company_group_id:activeCompanyGroup?.id
}),
);
}
@ -117,13 +135,27 @@ export function CatalogueJurTable() {
dispatch(
getCatalogueJurThunk({ closed: isGettingArchive, outside_group: true }),
);
closeEditCatalogueJurHandler()
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();
}, [isGettingArchive]);
}, [isGettingArchive, activeCompanyGroup]);
useEffect(() => {
getRelatedGroupsHandler();
}, [activeCompanyGroup]);
const extraColumns: ColumnsType<ICatalogueJur> = useMemo(
() => [
@ -182,23 +214,62 @@ export function CatalogueJurTable() {
);
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
height: "100%",
background: "white",
borderRadius: 12,
}}
>
{!outside_group && (
<div
style={{ padding: "12px 16px", borderBottom: "1px solid #0f172a0f" }}
>
<Title level={5} style={{ margin: 0 }}>
{activeCompanyGroup?.name}
</Title>
<Text type="secondary" style={{ fontSize: 13 }}>
{activeCompanyGroup?.base_regional_office_name ?? ""}
{activeCompanyGroup?.legal_entities_count} организаций
</Text>
</div>
)}
<Space
style={{
padding: "16px 12px",
padding: "0 12px",
width: "100%",
justifyContent: "space-between",
borderBottom: "1px",
}}
>
<Input
allowClear
prefix={<SearchOutlined />}
placeholder="Поиск по ГК, CRM ID или РФ"
value={rawQuery}
onChange={(e) => setRawQuery(e.target.value)}
style={{ maxWidth: 320 }}
/>
{outside_group ? (
<Input
allowClear
prefix={<SearchOutlined />}
placeholder="Поиск по ГК, CRM ID или РФ"
value={rawQuery}
onChange={(e) => setRawQuery(e.target.value)}
style={{ maxWidth: 320 }}
/>
) : (
<Tabs
activeKey={activeTab}
onChange={(k) => setActiveTab(k as TabKey)}
style={{ marginBottom: 0 }}
items={[
{
key: "legal_entities",
label: `ЮЛ ${activeCompanyGroup?.legal_entities_count}`,
},
{ key: "gsk", label: `ГСК ${gskData?.length ?? 0}` },
{ key: "gsz", label: `ГСЗ ${gszData?.length ?? 0}` },
]}
/>
)}
<Space>
{hasActiveFilters && (
<Button
@ -210,13 +281,14 @@ export function CatalogueJurTable() {
Сбросить фильтры
</Button>
)}
<Checkbox
checked={isGettingArchive}
onChange={(e) => dispatch(setIsGettingArchive(e.target.checked))}
>
Архив
</Checkbox>
{activeTab === "legal_entities" && (
<Checkbox
checked={isGettingArchive}
onChange={(e) => dispatch(setIsGettingArchive(e.target.checked))}
>
Архив
</Checkbox>
)}
<ColumnSettings
defs={catalogueJurConfig.columns}
visibleKeys={visibleKeys}
@ -224,7 +296,6 @@ export function CatalogueJurTable() {
/>
</Space>
</Space>
<DataTable
sortState={sort}
onSortChange={setSort}
@ -267,7 +338,7 @@ export function CatalogueJurTable() {
jurId={jurId}
open={editCatalogueFormOpen}
onCancel={() => {
closeEditCatalogueJurHandler()
closeEditCatalogueJurHandler();
}}
onOk={(values) => {
editJurHandler(values);

View File

@ -4,7 +4,7 @@ import { ICatalogueJur } from "../../types/catalogueJur/catalogueJur";
export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
rowKey: "id",
searchableKeys: ["closed", "inn", "kpp", "name", "company_group_name", "entity_type"],
searchableKeys: ["closed", "inn", "kpp", "name", "entity_type"],
columns: [
{
key: "closed",
@ -30,11 +30,6 @@ export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
title: "Организация",
features: { search: true, multiselect: true, sort: true },
},
{
key: "company_group_name",
title: "ГК",
features: { search: true, multiselect: true, sort: true },
},
{
key: "entity_type",
title: "Тип",

View File

@ -0,0 +1,186 @@
import { useEffect, useMemo, useState } from "react";
import { Input, List, Spin, Empty, Typography, Tag } from "antd";
import { FolderOpenOutlined, SearchOutlined } from "@ant-design/icons";
import { useAppDispatch, useAppSelector } from "../../store/store";
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
import { setActiveCompanyGroup } from "../../store/slices/companyGroupsSlice/companyGroupsSlice";
const { Text } = Typography;
export const CompanyGroupsList = () => {
const dispatch = useAppDispatch();
const rawRows = useAppSelector((state) => state.company_groups.raw_rows);
const loading = useAppSelector((state) => state.company_groups.loading);
const activeCompanyGroup = useAppSelector(
(state) => state.company_groups.activeCompanyGroup,
);
const [query, setQuery] = useState("");
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return rawRows;
return rawRows.filter(
(g) =>
g.name.toLowerCase().includes(q) || g.crm_id?.toLowerCase().includes(q),
);
}, [rawRows, query]);
useEffect(() => {
dispatch(getCompanyGroupsThunk());
}, [dispatch]);
useEffect(() => {
if (!activeCompanyGroup && rawRows?.length) {
dispatch(setActiveCompanyGroup(rawRows?.[0]));
}
}, [rawRows, activeCompanyGroup]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
height: "100%",
background: "#fff",
borderRight: "1px solid #f0f0f0",
borderRadius: 12,
}}
>
<div style={{ padding: "12px 16px", borderBottom:"1px solid #0f172a0f" }}>
<Input
allowClear
prefix={<SearchOutlined style={{ color: "#bfbfbf" }} />}
placeholder={"Поиск по ЮЛ или ГСК"}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<div style={{ flex: 1, overflowY: "auto", padding: "10px" }}>
{loading ? (
<div style={{ textAlign: "center", padding: 24 }}>
<Spin />
</div>
) : filtered.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="Компании и организации не найдены"
style={{ marginTop: 32 }}
/>
) : (
<List
dataSource={filtered}
split={false}
renderItem={(item) => {
const active = item.id === activeCompanyGroup?.id;
return (
<List.Item
onClick={() => dispatch(setActiveCompanyGroup(item))}
style={{
cursor: "pointer",
borderRadius: 8,
padding: "10px 12px",
marginBottom: 4,
background: active ? "#e6f4ea" : "transparent",
boxShadow: active
? "inset 0 0 0 1px #2ea64b3d, inset 3px 0 0 #258141"
: "inset 0 0 0 1px #0f172a14",
transition: "background 0.15s",
}}
onMouseEnter={(e) => {
if (!active) {
(e.currentTarget as HTMLElement).style.background =
"#fafafa";
}
}}
onMouseLeave={(e) => {
if (!active) {
(e.currentTarget as HTMLElement).style.background =
"transparent";
}
}}
>
<div
style={{
display: "flex",
alignItems: "center",
width: "100%",
gap: 12,
minWidth: 0,
}}
>
<Tag
style={{
borderRadius: 8,
width: 28,
height: 28,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
color={"success"}
>
<FolderOpenOutlined
style={{
fontSize: 18,
color: "green",
flexShrink: 0,
}}
/>
</Tag>
<div style={{ flex: 1, minWidth: 0 }}>
<Text
strong
ellipsis
style={{
display: "block",
fontSize: 14,
lineHeight: "20px",
}}
>
{item.name}
</Text>
<Text
type="secondary"
ellipsis
style={{
display: "block",
fontSize: 12,
lineHeight: "16px",
}}
>
CRM ID: {item.crm_id ?? "-"}
</Text>
</div>
<Tag
style={{
margin: 0,
borderRadius: 10,
background: "#f5f5f5",
border: "none",
color: "#595959",
fontWeight: 500,
minWidth: 24,
height: 20,
lineHeight: "20px",
textAlign: "center",
padding: "0 8px",
flexShrink: 0,
}}
>
{item.legal_entities_count}
</Tag>
</div>
</List.Item>
);
}}
/>
)}
</div>
</div>
);
};

View File

@ -46,7 +46,7 @@ export function CompanyGroupsTable() {
);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%'}}>
<Space
style={{
padding: '16px 12px',