aurora-(1636, 1693): обновление справочника гк и юл и исправлена фильтрация юл без гк

This commit is contained in:
Grog 2026-10-27 17:04:18 +03:00
parent ee43d5200a
commit c64975241b
16 changed files with 275 additions and 63 deletions

View File

@ -4,7 +4,13 @@ import { useEffect, useState } from 'react';
import { ApiError } from '../api/client'; import { ApiError } from '../api/client';
import PageTitle from '../components/PageTitle'; import PageTitle from '../components/PageTitle';
import { createCatalogueJur } from '../services/api/directories/catalogue_jur'; import { createCatalogueJur } from '../services/api/directories/catalogue_jur';
import { createCompanyGroup, fetchRelatedGroups } from '../services/api/directories/company_groups'; import {
archiveCompanyGroup,
createCompanyGroup,
editCompanyGroup,
fetchRelatedGroups,
unarchiveCompanyGroup,
} from '../services/api/directories/company_groups';
import { createRelatedGroup } from '../services/api/directories/related-groups'; import { createRelatedGroup } from '../services/api/directories/related-groups';
import { setActiveCompanyGroup } from '../store/slices/catalogueJurSlice/catalogueJurSlice'; import { setActiveCompanyGroup } from '../store/slices/catalogueJurSlice/catalogueJurSlice';
import { getCatalogueJurThunk } from '../store/slices/catalogueJurSlice/thunks'; import { getCatalogueJurThunk } from '../store/slices/catalogueJurSlice/thunks';
@ -12,7 +18,7 @@ import { getCompanyGroupsThunk } from '../store/slices/companyGroupsSlice/thunks
import { useAppDispatch, useAppSelector } from '../store/store'; import { useAppDispatch, useAppSelector } from '../store/store';
import type { RelatedGroupOut } from '../types/api'; import type { RelatedGroupOut } from '../types/api';
import type { CatalogueJurFormProps } from '../types/catalogueJur/catalogueJur'; import type { CatalogueJurFormProps } from '../types/catalogueJur/catalogueJur';
import type { CreateCompanyGroupProps } from '../types/companyGroups/companyGroups'; import type { CompanyGroupFormProps } from '../types/companyGroups/companyGroups';
import type { CreateEntityKind, RelatedGroupFormType } from '../types/relatedGroup/relatedGroup'; import type { CreateEntityKind, RelatedGroupFormType } from '../types/relatedGroup/relatedGroup';
import { useNotificationApi } from '../utils/useNotification'; import { useNotificationApi } from '../utils/useNotification';
import { CataloguGroupTabs, type CatalogueGroupTabKey } from '../widgets/CataloguGroupTabs/CataloguGroupTabs'; import { CataloguGroupTabs, type CatalogueGroupTabKey } from '../widgets/CataloguGroupTabs/CataloguGroupTabs';
@ -24,6 +30,7 @@ import { CompanyGroupsList } from '../widgets/CompanyGroupsList/CompanyGroupsLis
import { CreateEntityPickerModal } from '../widgets/CreateEntityPickerModal/CreateEntityPickerModal'; import { CreateEntityPickerModal } from '../widgets/CreateEntityPickerModal/CreateEntityPickerModal';
import { RelatedGroupForm } from '../widgets/RelatedGroupForm/RelatedGroupForm'; import { RelatedGroupForm } from '../widgets/RelatedGroupForm/RelatedGroupForm';
import { RelatedGroupTable } from '../widgets/RelatedGroupTable/RelatedGroupTable'; import { RelatedGroupTable } from '../widgets/RelatedGroupTable/RelatedGroupTable';
import { ArchiveCompanyGroupModal } from '../widgets/ArchiveCompanyGroupModal/ArchiveCompanyGroupModal';
export function CatalogueGroup() { export function CatalogueGroup() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -34,16 +41,20 @@ export function CatalogueGroup() {
const [entityCreateModalOpen, setEntityCreateModalOpen] = useState(false); const [entityCreateModalOpen, setEntityCreateModalOpen] = useState(false);
const [kindForm, setKindForm] = useState<CreateEntityKind | null>(null); const [kindForm, setKindForm] = useState<CreateEntityKind | null>(null);
const [relatedGroupLoading, setRelatedGroupLoading] = useState(false); const [relatedGroupLoading, setRelatedGroupLoading] = useState(false);
const [companyGroupEditFormOpen, setCompanyGroupEditFormOpen] = useState(false);
const [companyGroupDeleteOpen, setCompanyGroupDeleteOpen] = useState(false);
const [companyGroupDeleteLoading, setCompanyGroupDeleteLoading] = useState(false);
const [activeTab, setActiveTab] = useState<CatalogueGroupTabKey>('legal_entities'); const [activeTab, setActiveTab] = useState<CatalogueGroupTabKey>('legal_entities');
const [gskData, setGskData] = useState<RelatedGroupOut[]>([]); const [gskData, setGskData] = useState<RelatedGroupOut[]>([]);
const [gszData, setGszData] = useState<RelatedGroupOut[]>([]); const [gszData, setGszData] = useState<RelatedGroupOut[]>([]);
const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup); const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup);
const relatedGroupFilter = useAppSelector((state) => state.catalogue_jur.relatedGroupFilter); const relatedGroupFilter = useAppSelector((state) => state.catalogue_jur.relatedGroupFilter);
const rawRows = useAppSelector((state) => state.company_groups.raw_rows); const rawRows = useAppSelector((state) => state.company_groups.raw_rows);
const isGettingArchiveCompany = useAppSelector((state) => state.company_groups.isGettingArchive);
async function createJurHandler(values: CatalogueJurFormProps) { async function createJurHandler(values: CatalogueJurFormProps) {
await createCatalogueJur(values); await createCatalogueJur(values);
dispatch(getCompanyGroupsThunk()); dispatch(getCompanyGroupsThunk({ is_archived: isGettingArchiveCompany }));
dispatch( dispatch(
getCatalogueJurThunk({ getCatalogueJurThunk({
closed: isGettingArchive, closed: isGettingArchive,
@ -63,12 +74,43 @@ export function CatalogueGroup() {
setKindForm(null); setKindForm(null);
} }
} }
async function createCompanyGroupHandler(values: CreateCompanyGroupProps) { async function createCompanyGroupHandler(values: CompanyGroupFormProps) {
await createCompanyGroup(values); await createCompanyGroup(values);
dispatch(getCompanyGroupsThunk()); dispatch(getCompanyGroupsThunk({ is_archived: isGettingArchiveCompany }));
getRelatedGroupsHandler();
setKindForm(null); setKindForm(null);
} }
async function editCompanyGroupHandler(values: CompanyGroupFormProps) {
if (activeCompanyGroup?.id) {
await editCompanyGroup(activeCompanyGroup?.id, values);
dispatch(getCompanyGroupsThunk({ is_archived: isGettingArchiveCompany }));
setCompanyGroupEditFormOpen(false);
}
}
async function archiveCompanyGroupHandler() {
if (!activeCompanyGroup) {
return;
}
try {
setCompanyGroupDeleteLoading(true);
if (activeCompanyGroup?.is_archived) {
await unarchiveCompanyGroup(activeCompanyGroup?.id);
} else {
await archiveCompanyGroup(activeCompanyGroup.id);
}
dispatch(getCompanyGroupsThunk({ is_archived: isGettingArchiveCompany }));
setCompanyGroupDeleteOpen(false);
} catch (e) {
if (e instanceof ApiError) {
notification.error({
description: e.message,
});
}
} finally {
setCompanyGroupDeleteLoading(false);
}
}
async function getRelatedGroupsHandler() { async function getRelatedGroupsHandler() {
if (activeCompanyGroup?.id) { if (activeCompanyGroup?.id) {
@ -93,7 +135,6 @@ export function CatalogueGroup() {
getRelatedGroupsHandler(); getRelatedGroupsHandler();
}, [activeCompanyGroup]); }, [activeCompanyGroup]);
// Без выбранной ГК таблица ЮЛ не грузится — выбираем первую ГК списка
useEffect(() => { useEffect(() => {
if (!activeCompanyGroup && rawRows?.length) { if (!activeCompanyGroup && rawRows?.length) {
dispatch(setActiveCompanyGroup(rawRows[0])); dispatch(setActiveCompanyGroup(rawRows[0]));
@ -140,7 +181,7 @@ export function CatalogueGroup() {
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden', overflow: 'hidden',
}}> }}>
<CompanyGroupHeader /> <CompanyGroupHeader setDeleteConfirmOpen={setCompanyGroupDeleteOpen} setEditFormOpen={setCompanyGroupEditFormOpen} />
{activeTab === 'legal_entities' && ( {activeTab === 'legal_entities' && (
<CatalogueJurTable <CatalogueJurTable
outside_group={false} outside_group={false}
@ -200,6 +241,25 @@ export function CatalogueGroup() {
onOk={createCompanyGroupHandler} onOk={createCompanyGroupHandler}
/> />
)} )}
{companyGroupEditFormOpen && (
<CompanyGroupForm
open={companyGroupEditFormOpen}
onCancel={() => {
setCompanyGroupEditFormOpen(false);
}}
onOk={editCompanyGroupHandler}
companyGroupId={activeCompanyGroup?.id}
/>
)}
{companyGroupDeleteOpen && activeCompanyGroup && (
<ArchiveCompanyGroupModal
open={companyGroupDeleteOpen}
loading={companyGroupDeleteLoading}
onCancel={() => setCompanyGroupDeleteOpen(false)}
onOk={archiveCompanyGroupHandler}
isArchived={activeCompanyGroup?.is_archived}
/>
)}
</div> </div>
); );
} }

View File

@ -1,15 +1,20 @@
import { api } from '../../../api/client'; import { api } from '../../../api/client';
import { GetCompanyGroupsPropsI } from '../../../store/slices/companyGroupsSlice/types';
import type { LegalEntityBrief, Page, RelatedGroupDetail, RelatedGroupOut } from '../../../types/api'; import type { LegalEntityBrief, Page, RelatedGroupDetail, RelatedGroupOut } from '../../../types/api';
import type { CreateCompanyGroupProps, ICompanyGroupOut } from '../../../types/companyGroups/companyGroups'; import type { CompanyGroupFormProps, ICompanyGroupOut } from '../../../types/companyGroups/companyGroups';
const CG_PAGE = 200; const CG_PAGE = 200;
export const LE_PAGE = 200; export const LE_PAGE = 200;
/** Все видимые ГК — дочитываем страницы, пока не получим total. */ /** Все видимые ГК — дочитываем страницы, пока не получим total. */
export async function fetchCompanyGroups(): Promise<ICompanyGroupOut[]> { export async function fetchCompanyGroups(params: GetCompanyGroupsPropsI): Promise<ICompanyGroupOut[]> {
const all: ICompanyGroupOut[] = []; const all: ICompanyGroupOut[] = [];
for (let offset = 0; ; offset += CG_PAGE) { for (let offset = 0; ; offset += CG_PAGE) {
const page = await api.get<Page<ICompanyGroupOut>>(`/directories/company-groups?limit=${CG_PAGE}&offset=${offset}`); const page = await api.get<Page<ICompanyGroupOut>>(`/directories/company-groups`, {
limit: CG_PAGE,
offset: offset,
...params,
});
all.push(...page.items); all.push(...page.items);
if (all.length >= page.total || page.items.length === 0) { if (all.length >= page.total || page.items.length === 0) {
return all; return all;
@ -37,6 +42,15 @@ 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 function createCompanyGroup(body: CreateCompanyGroupProps): Promise<void> { export function createCompanyGroup(body: CompanyGroupFormProps): Promise<void> {
return api.post<void>('/directories/company-groups', body); return api.post<void>('/directories/company-groups', body);
} }
export function editCompanyGroup(id: string, body: CompanyGroupFormProps): Promise<void> {
return api.patch<void>(`/directories/company-groups/${id}`, body);
}
export function archiveCompanyGroup(id: string): Promise<void> {
return api.post<void>(`/directories/company-groups/${id}/archive`);
}
export function unarchiveCompanyGroup(id: string): Promise<void> {
return api.post<void>(`/directories/company-groups/${id}/unarchive`);
}

View File

@ -1,4 +1,4 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getCompanyGroupsThunk } from './thunks'; import { getCompanyGroupsThunk } from './thunks';
import type { CompanyGroupSliceType } from './types'; import type { CompanyGroupSliceType } from './types';
@ -6,13 +6,18 @@ const initialState: CompanyGroupSliceType = {
raw_rows: [], raw_rows: [],
total_groups: 0, total_groups: 0,
loading: false, loading: false,
isGettingArchive: false,
error: null, error: null,
}; };
const comGroupsSlice = createSlice({ const comGroupsSlice = createSlice({
name: 'company_groups', name: 'company_groups',
initialState, initialState,
reducers: {}, reducers: {
setIsGettingArchive(state, action: PayloadAction<boolean>) {
state.isGettingArchive = action.payload;
},
},
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
.addCase(getCompanyGroupsThunk.pending, (state) => { .addCase(getCompanyGroupsThunk.pending, (state) => {
@ -30,4 +35,6 @@ const comGroupsSlice = createSlice({
}); });
}, },
}); });
export const { setIsGettingArchive } = comGroupsSlice.actions;
export default comGroupsSlice.reducer; export default comGroupsSlice.reducer;

View File

@ -1,14 +1,15 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { fetchCompanyGroups } from '../../../services/api/directories/company_groups'; import { fetchCompanyGroups } from '../../../services/api/directories/company_groups';
import { type ICompanyGroup, toCompanyGroupRow } from '../../../types/companyGroups/companyGroups'; import { type ICompanyGroup, toCompanyGroupRow } from '../../../types/companyGroups/companyGroups';
import { GetCompanyGroupsPropsI } from './types';
export const getCompanyGroupsThunk = createAsyncThunk<ICompanyGroup[], void, { rejectValue: string }>( export const getCompanyGroupsThunk = createAsyncThunk<ICompanyGroup[], GetCompanyGroupsPropsI | undefined, { rejectValue: string }>(
'company_groups/getAll', 'company_groups/getAll',
async (_, thunkApi) => { async (args = {}, thunkApi) => {
try { try {
return (await fetchCompanyGroups()).map(toCompanyGroupRow); return (await fetchCompanyGroups(args)).map(toCompanyGroupRow);
} catch (e) { } catch (e) {
return thunkApi.rejectWithValue(e instanceof Error ? e.message : 'Ошибка'); return thunkApi.rejectWithValue(e instanceof Error ? e.message : 'Ошибка');
} }
}, },
); )

View File

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

View File

@ -21,6 +21,7 @@ export interface ICatalogueJur {
name: string; name: string;
is_head_office: boolean; is_head_office: boolean;
}; };
regional_name?:string;
} }
export type KindType = 'gsk' | 'gsz'; export type KindType = 'gsk' | 'gsz';

View File

@ -6,6 +6,7 @@ export interface ICompanyGroupOut {
base_regional_office_id: string | null; base_regional_office_id: string | null;
base_regional_office: { id: string; name: string; is_head_office: boolean } | null; base_regional_office: { id: string; name: string; is_head_office: boolean } | null;
legal_entities_count: number; legal_entities_count: number;
is_archived:boolean;
} }
/** Строка таблицы ГК: имя филиала плоским полем — по нему работают поиск и фильтры GenericTable. */ /** Строка таблицы ГК: имя филиала плоским полем — по нему работают поиск и фильтры GenericTable. */
@ -18,7 +19,7 @@ export const toCompanyGroupRow = (g: ICompanyGroupOut): ICompanyGroup => ({
base_regional_office_name: g.base_regional_office?.name ?? '—', base_regional_office_name: g.base_regional_office?.name ?? '—',
}); });
export interface CreateCompanyGroupProps { export interface CompanyGroupFormProps {
name: string; name: string;
crm_id: string; crm_id: string;
base_regional_office_id: string | null; base_regional_office_id: string | null;

View File

@ -0,0 +1,26 @@
import { Modal } from 'antd';
type Props = {
open: boolean;
onCancel: () => void;
onOk: () => void | Promise<void>;
loading?: boolean;
isArchived: boolean;
};
export const ArchiveCompanyGroupModal: React.FC<Props> = ({ open, onCancel, onOk, loading, isArchived }) => {
return (
<Modal
title={isArchived ? 'Разархивировать' : 'Архивировать'}
open={open}
onCancel={onCancel}
onOk={onOk}
okText='Подтвердить'
cancelText='Отмена'
okButtonProps={{ danger: true }}
confirmLoading={loading}
centered>
<p style={{ margin: 0, color: '#595959' }}>Группа компании будет {isArchived ? 'разархивированна' : 'архивированна'}</p>
</Modal>
);
};

View File

@ -1,33 +1,74 @@
import { Space } from 'antd'; import { Button, Dropdown, Space } from 'antd';
import Text from 'antd/es/typography/Text'; import Text from 'antd/es/typography/Text';
import Title from 'antd/es/typography/Title'; import Title from 'antd/es/typography/Title';
import { useAppSelector } from '../../store/store'; import { useAppSelector } from '../../store/store';
import { pluralRu } from '../../utils/labels'; import { pluralRu } from '../../utils/labels';
import { DeleteOutlined, EditOutlined, EllipsisOutlined, HistoryOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
export const CompanyGroupHeader = () => { interface CompanyGroupHeaderI {
setDeleteConfirmOpen: (el: boolean) => void;
setEditFormOpen: (el: boolean) => void;
}
export const CompanyGroupHeader = ({ setEditFormOpen, setDeleteConfirmOpen }: CompanyGroupHeaderI) => {
const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup); const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup);
const count = activeCompanyGroup?.legal_entities_count ?? 0; const count = activeCompanyGroup?.legal_entities_count ?? 0;
const navigate = useNavigate();
const organizationWord = pluralRu(count, ['организация', 'организации', 'организаций']); const organizationWord = pluralRu(count, ['организация', 'организации', 'организаций']);
const items = [
{
key: 'edit',
icon: <EditOutlined />,
label: 'Изменить ГК',
onClick: () => {
setEditFormOpen(true);
},
},
{
key: 'history',
icon: <HistoryOutlined />,
label: 'История изменений',
onClick: () => navigate('/logging'),
},
{
key: 'delete',
icon: <DeleteOutlined />,
label: activeCompanyGroup?.is_archived ? 'Разархивировать' : 'Архивировать',
danger: true,
onClick: () => {
setDeleteConfirmOpen(true);
},
},
];
return ( return (
<div <Space
style={{ style={{
padding: '6px 16px', padding: '6px 16px',
borderBottom: '1px solid #0f172a0f', borderBottom: '1px solid #0f172a0f',
flexShrink: 0, flexShrink: 0,
justifyContent: 'space-between',
}}> }}>
<Title level={5} style={{ margin: 0 }}> <div>
{activeCompanyGroup?.name ?? ''} <Title level={5} style={{ margin: 0 }}>
</Title> {activeCompanyGroup?.name ?? ''}
<Space> </Title>
<Text type='secondary' style={{ fontSize: 13 }}> <Space>
{activeCompanyGroup?.base_regional_office_name ?? ''} <Text type='secondary' style={{ fontSize: 13 }}>
</Text> {activeCompanyGroup?.base_regional_office_name ?? ''}
<Text type='secondary' style={{ fontSize: 13 }}> </Text>
{count ?? 0} {organizationWord} <Text type='secondary' style={{ fontSize: 13 }}>
</Text> {count ?? 0} {organizationWord}
</Space> </Text>
</div> </Space>
</div>
<Dropdown menu={{ items }} trigger={['click']}>
<Button type='default' style={{ width: '32px' }}>
<EllipsisOutlined />
</Button>
</Dropdown>
</Space>
); );
}; };

View File

@ -52,14 +52,16 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur
const [editCatalogueFormOpen, setEditCatalogueFormOpen] = useState(false); const [editCatalogueFormOpen, setEditCatalogueFormOpen] = useState(false);
const [withdrawCatalogueFormOpen, setWithdrawCatalogueFormOpen] = useState(false); const [withdrawCatalogueFormOpen, setWithdrawCatalogueFormOpen] = useState(false);
const [jurId, setJurId] = useState(''); const [jurId, setJurId] = useState('');
const isGettingArchiveCompany = useAppSelector((state) => state.company_groups.isGettingArchive);
const filterdData = useMemo(() => { const filterdData = useMemo(() => {
let newData = rawRows
if (relatedGroupFilter?.id && !outside_group) { if (relatedGroupFilter?.id && !outside_group) {
return rawRows?.filter((row) => { newData = rawRows?.filter((row) => {
return row?.memberships?.some((member) => member.related_group_id === relatedGroupFilter.id); return row?.memberships?.some((member) => member.related_group_id === relatedGroupFilter.id);
}); });
} }
return rawRows; return newData?.map((elem) => elem && { ...elem, regional_name: elem?.regional_office?.name });
}, [rawRows, relatedGroupFilter, outside_group]); }, [rawRows, relatedGroupFilter, outside_group]);
const { visibleKeys, setVisibleKeys } = useVisibleColumns(catalogueJurConfig); const { visibleKeys, setVisibleKeys } = useVisibleColumns(catalogueJurConfig);
@ -67,7 +69,7 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur
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(filterdData, catalogueJurConfig, rawQuery, columnFilters, sort); const { rows } = useFilteredRows(filterdData, catalogueJurConfig, rawQuery, columnFilters, sort);
function getCatalogueJur() { function getCatalogueJur() {
dispatch( dispatch(
getCatalogueJurThunk({ getCatalogueJurThunk({
@ -95,7 +97,7 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur
try { try {
await moveCatalogueJur(jurId, values); await moveCatalogueJur(jurId, values);
getCatalogueJur(); getCatalogueJur();
dispatch(getCompanyGroupsThunk()); dispatch(getCompanyGroupsThunk({ is_archived: isGettingArchiveCompany }));
closeMoveCatalogueJurHandler(); closeMoveCatalogueJurHandler();
} catch (e) { } catch (e) {
if (e instanceof ApiError) { if (e instanceof ApiError) {
@ -109,7 +111,7 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur
try { try {
await withdrawCatalogueJur(jurId, values); await withdrawCatalogueJur(jurId, values);
getCatalogueJur(); getCatalogueJur();
dispatch(getCompanyGroupsThunk()); dispatch(getCompanyGroupsThunk({ is_archived: isGettingArchiveCompany }));
closeWithdrawCatalogueJurHandler(); closeWithdrawCatalogueJurHandler();
} catch (e) { } catch (e) {
@ -128,7 +130,6 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur
} }
useEffect(() => { useEffect(() => {
// «ЮЛ вне ГК» грузится без выбранной ГК; список ГК — только по выбранной (без неё запрос ушёл бы по всем ГК)
if (outside_group || activeCompanyGroup?.id) { if (outside_group || activeCompanyGroup?.id) {
getCatalogueJur(); getCatalogueJur();
} }
@ -244,7 +245,7 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur
justifyContent: 'space-between', justifyContent: 'space-between',
gap: 16, gap: 16,
padding: '10px 16px', padding: '10px 16px',
background: '#fff8e1', // светло-жёлтый фон background: '#fff8e1',
borderTop: '1px solid #ffe58f', borderTop: '1px solid #ffe58f',
borderBottom: '1px solid #ffe58f', borderBottom: '1px solid #ffe58f',
borderRadius: 8, borderRadius: 8,

View File

@ -5,7 +5,7 @@ import type { ICatalogueJur } from '../../types/catalogueJur/catalogueJur';
export const catalogueJurConfig: TableConfig<ICatalogueJur> = { export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
rowKey: 'id', rowKey: 'id',
searchableKeys: ['closed', 'inn', 'kpp', 'name', 'entity_type'], searchableKeys: ['closed', 'inn', 'crm_id', 'kpp', 'name', 'entity_type', 'regional_name'],
columns: [ columns: [
{ {
key: 'closed', key: 'closed',
@ -18,6 +18,28 @@ export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
return <TableTag text={text} {...preset} />; return <TableTag text={text} {...preset} />;
}, },
}, },
{
key: 'crm_id',
title: 'Входит в ГСК',
render: (_: unknown, row) => {
const has = row?.memberships?.some((elem) => elem.kind === 'gsk');
const preset = has ? TAG_PRESETS.green : TAG_PRESETS.gray;
const text = has ? 'Входит' : 'Не входит';
return <TableTag text={text} {...preset} />;
},
},
{
key: 'company_group_id',
title: 'Входит в ГСЗ',
render: (_: unknown, row) => {
const has = row?.memberships?.some((elem) => elem.kind === 'gsz');
const preset = has ? TAG_PRESETS.green : TAG_PRESETS.gray;
const text = has ? 'Входит' : 'Не входит';
return <TableTag text={text} {...preset} />;
},
},
{ {
key: 'inn', key: 'inn',
title: 'ИНН', title: 'ИНН',
@ -43,9 +65,9 @@ export const catalogueJurConfig: TableConfig<ICatalogueJur> = {
}, },
}, },
{ {
key: 'regional_office', key: 'regional_name',
title: 'Основной РФ', title: 'Основной РФ',
render: (val) => (val as ICatalogueJur['regional_office'] | null)?.name, features: { search: true, multiselect: true, sort: true },
}, },
], ],
}; };

View File

@ -3,26 +3,47 @@ import type React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ApiError } from '../../api/client'; import { ApiError } from '../../api/client';
import { useMe } from '../../auth/useMe'; import { useMe } from '../../auth/useMe';
import { useAppSelector } from '../../store/store'; import type { CompanyGroupFormProps } from '../../types/companyGroups/companyGroups';
import type { CreateCompanyGroupProps } from '../../types/companyGroups/companyGroups';
import { useNotificationApi } from '../../utils/useNotification'; import { useNotificationApi } from '../../utils/useNotification';
import { loadOffices } from '../Admin/UsersTable'; import { loadOffices } from '../Admin/UsersTable';
import { fetchOneCompanyGroup } from '../../services/api/directories/company_groups';
type Props = { type Props = {
open: boolean; open: boolean;
onCancel: () => void; onCancel: () => void;
onOk: (values: CreateCompanyGroupProps) => void | Promise<void>; onOk: (values: CompanyGroupFormProps) => void | Promise<void>;
companyGroupId?: string;
}; };
export const CompanyGroupForm: React.FC<Props> = ({ open, onCancel, onOk }) => { export const CompanyGroupForm: React.FC<Props> = ({ open, onCancel, onOk, companyGroupId }) => {
const [form] = Form.useForm<CreateCompanyGroupProps>(); const [form] = Form.useForm<CompanyGroupFormProps>();
const { me } = useMe(); const { me } = useMe();
const notification = useNotificationApi(); const notification = useNotificationApi();
const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup);
const [officesOptions, setOfficesOptions] = useState<{ value: string; label: string }[]>([]); const [officesOptions, setOfficesOptions] = useState<{ value: string; label: string }[]>([]);
const [loadingOptions, setLoadingOptions] = useState(false); const [loadingOptions, setLoadingOptions] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const [initialDataLoading, setInitialDataLoading] = useState(false);
async function getInitialData() {
if (companyGroupId) {
setInitialDataLoading(true);
try {
const data = await fetchOneCompanyGroup(companyGroupId);
form.setFieldsValue({
name: data.name,
crm_id: data.crm_id ?? undefined,
base_regional_office_id: data.base_regional_office_id,
});
} catch (e) {
if (e instanceof ApiError) {
notification.error({ description: e.message });
}
} finally {
setInitialDataLoading(false);
}
}
}
async function getOfficesOptionsHandler() { async function getOfficesOptionsHandler() {
setLoadingOptions(true); setLoadingOptions(true);
@ -45,6 +66,7 @@ export const CompanyGroupForm: React.FC<Props> = ({ open, onCancel, onOk }) => {
const values = await form.validateFields(); const values = await form.validateFields();
await onOk({ await onOk({
...values, ...values,
crm_id: values?.crm_id ?? null,
base_regional_office_id: values?.base_regional_office_id ? values?.base_regional_office_id : (me?.regional_office.id as string), base_regional_office_id: values?.base_regional_office_id ? values?.base_regional_office_id : (me?.regional_office.id as string),
}); });
} catch (e) { } catch (e) {
@ -72,18 +94,20 @@ export const CompanyGroupForm: React.FC<Props> = ({ open, onCancel, onOk }) => {
}; };
useEffect(() => { useEffect(() => {
getInitialData();
getOfficesOptionsHandler(); getOfficesOptionsHandler();
}, [activeCompanyGroup]); }, []);
return ( return (
<Modal <Modal
title='Создать группу компаний' title={`${companyGroupId ? 'Редактировать' : 'Создать'} группу компаний`}
open={open} open={open}
onCancel={handleCancel} onCancel={handleCancel}
onOk={handleOk} onOk={handleOk}
okText='Создать' okText={`${companyGroupId ? 'Редактировать' : 'Создать'}`}
cancelText='Отмена' cancelText='Отмена'
destroyOnHidden destroyOnHidden
loading={initialDataLoading}
confirmLoading={confirmLoading} confirmLoading={confirmLoading}
width={720}> width={720}>
<Form form={form} layout='vertical'> <Form form={form} layout='vertical'>

View File

@ -1,10 +1,11 @@
import { FolderOpenOutlined, SearchOutlined } from '@ant-design/icons'; import { FolderOpenOutlined, SearchOutlined } from '@ant-design/icons';
import { Empty, Input, List, Spin, Tag, Typography } from 'antd'; import { Checkbox, Empty, Input, List, Space, Spin, Tag, Typography } from 'antd';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useVirtualList } from '../../hooks/useVirtualList'; import { useVirtualList } from '../../hooks/useVirtualList';
import { setActiveCompanyGroup } from '../../store/slices/catalogueJurSlice/catalogueJurSlice'; import { setActiveCompanyGroup } from '../../store/slices/catalogueJurSlice/catalogueJurSlice';
import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks'; import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks';
import { useAppDispatch, useAppSelector } from '../../store/store'; import { useAppDispatch, useAppSelector } from '../../store/store';
import { setIsGettingArchive } from '../../store/slices/companyGroupsSlice/companyGroupsSlice';
const { Text } = Typography; const { Text } = Typography;
@ -14,7 +15,7 @@ export const CompanyGroupsList = () => {
const rawRows = useAppSelector((state) => state.company_groups.raw_rows); const rawRows = useAppSelector((state) => state.company_groups.raw_rows);
const loading = useAppSelector((state) => state.company_groups.loading); const loading = useAppSelector((state) => state.company_groups.loading);
const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup); const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup);
const isGettingArchiveCompany = useAppSelector((state) => state.company_groups.isGettingArchive);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const filtered = useMemo(() => { const filtered = useMemo(() => {
@ -30,8 +31,8 @@ export const CompanyGroupsList = () => {
}); });
useEffect(() => { useEffect(() => {
dispatch(getCompanyGroupsThunk()); dispatch(getCompanyGroupsThunk({is_archived:isGettingArchiveCompany}));
}, [dispatch]); }, [dispatch, isGettingArchiveCompany]);
useEffect(() => { useEffect(() => {
if (!activeCompanyGroup && rawRows?.length) { if (!activeCompanyGroup && rawRows?.length) {
@ -57,7 +58,7 @@ export const CompanyGroupsList = () => {
borderRight: '1px solid #f0f0f0', borderRight: '1px solid #f0f0f0',
borderRadius: 12, borderRadius: 12,
}}> }}>
<div style={{ padding: 12 }}> <Space style={{ padding: 12 }}>
<Input <Input
allowClear allowClear
prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />} prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
@ -65,7 +66,10 @@ export const CompanyGroupsList = () => {
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
/> />
</div> <Checkbox checked={isGettingArchiveCompany} onChange={(e) =>dispatch(setIsGettingArchive(e.target.checked))}>
Архив
</Checkbox>
</Space>
<div ref={ref} onScroll={onScroll} style={{ flex: 1, overflowY: 'auto', padding: '0 8px 8px' }}> <div ref={ref} onScroll={onScroll} style={{ flex: 1, overflowY: 'auto', padding: '0 8px 8px' }}>
{filtered.length === 0 ? ( {filtered.length === 0 ? (

View File

@ -29,6 +29,7 @@ export const MoveCatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, ju
const [form] = Form.useForm<FormValues>(); const [form] = Form.useForm<FormValues>();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const notification = useNotificationApi(); const notification = useNotificationApi();
const isGettingArchiveCompany = useAppSelector((state) => state.company_groups.isGettingArchive);
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);
@ -76,7 +77,7 @@ export const MoveCatalogueJurForm: React.FC<Props> = ({ open, onCancel, onOk, ju
}; };
useEffect(() => { useEffect(() => {
if (open && raw_rows.length === 0 && !loading) { if (open && raw_rows.length === 0 && !loading) {
dispatch(getCompanyGroupsThunk()); dispatch(getCompanyGroupsThunk({ is_archived: isGettingArchiveCompany }));
} }
}, [open, raw_rows.length, loading, dispatch]); }, [open, raw_rows.length, loading, dispatch]);

View File

@ -27,6 +27,7 @@ export const RelatedGroupForm: React.FC<Props> = ({ open, onCancel, onOk, relate
const companyGrops = useAppSelector((state) => state.company_groups.raw_rows); const companyGrops = useAppSelector((state) => state.company_groups.raw_rows);
const companyGropsLoading = useAppSelector((state) => state.company_groups.loading); const companyGropsLoading = useAppSelector((state) => state.company_groups.loading);
const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup); const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup);
const isGettingArchiveCompany = useAppSelector((state) => state.company_groups.isGettingArchive);
async function getInitialData() { async function getInitialData() {
if (!relatedGroupId) { if (!relatedGroupId) {
@ -38,7 +39,8 @@ export const RelatedGroupForm: React.FC<Props> = ({ open, onCancel, onOk, relate
setInitialDataLoading(true); setInitialDataLoading(true);
try { try {
const data = await fetchRelatedGroup(relatedGroupId); const data = await fetchRelatedGroup(relatedGroupId);
await dispatch(getCompanyGroupsThunk()); await dispatch(getCompanyGroupsThunk({ is_archived: isGettingArchiveCompany }));
form.setFieldsValue({ form.setFieldsValue({
crm_id: data?.crm_id, crm_id: data?.crm_id,
name: data?.name, name: data?.name,

View File

@ -1,4 +1,4 @@
import { EllipsisOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, EllipsisOutlined } from '@ant-design/icons';
import { Button, Dropdown, Space } from 'antd'; import { Button, Dropdown, Space } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { type ReactNode, useMemo, useState } from 'react'; import { type ReactNode, useMemo, useState } from 'react';
@ -77,6 +77,7 @@ export function RelatedGroupTable({ tabsComponent, data, loading, getData, activ
const items = [ const items = [
{ {
key: 'edit', key: 'edit',
icon: <EditOutlined />,
label: 'Изменить', label: 'Изменить',
onClick: () => { onClick: () => {
setGroupId(row.id); setGroupId(row.id);
@ -85,6 +86,7 @@ export function RelatedGroupTable({ tabsComponent, data, loading, getData, activ
}, },
{ {
key: 'delete', key: 'delete',
icon: <DeleteOutlined />,
label: 'Удалить', label: 'Удалить',
danger: true, danger: true,
onClick: () => { onClick: () => {