diff --git a/web/src/layouts/CatalogueGroups.tsx b/web/src/layouts/CatalogueGroups.tsx index 6d712ea..421b406 100644 --- a/web/src/layouts/CatalogueGroups.tsx +++ b/web/src/layouts/CatalogueGroups.tsx @@ -1,20 +1,28 @@ import { PlusOutlined } from '@ant-design/icons'; -import { Button, Space } from 'antd'; -import { useEffect, useState } from 'react'; +import { Button, Empty, Space } from 'antd'; +import { useEffect, useMemo, useState } from 'react'; import { ApiError } from '../api/client'; import PageTitle from '../components/PageTitle'; 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 { setActiveCompanyGroup } from '../store/slices/catalogueJurSlice/catalogueJurSlice'; import { getCatalogueJurThunk } from '../store/slices/catalogueJurSlice/thunks'; +import { nextActiveCompanyGroup, visibleCompanyGroups } from '../store/slices/companyGroupsSlice/selectors'; import { getCompanyGroupsThunk } from '../store/slices/companyGroupsSlice/thunks'; import { useAppDispatch, useAppSelector } from '../store/store'; import type { RelatedGroupOut } from '../types/api'; 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 { useNotificationApi } from '../utils/useNotification'; +import { ArchiveCompanyGroupModal } from '../widgets/ArchiveCompanyGroupModal/ArchiveCompanyGroupModal'; import { CataloguGroupTabs, type CatalogueGroupTabKey } from '../widgets/CataloguGroupTabs/CataloguGroupTabs'; import { CompanyGroupHeader } from '../widgets/CatalogueGroupHeader/CatalogueGroupHeader'; import { CatalogueJurForm } from '../widgets/CatalogueJurForm/CatalogueJurForm'; @@ -34,12 +42,18 @@ export function CatalogueGroup() { const [entityCreateModalOpen, setEntityCreateModalOpen] = useState(false); const [kindForm, setKindForm] = useState(null); const [relatedGroupLoading, setRelatedGroupLoading] = useState(false); + const [companyGroupEditFormOpen, setCompanyGroupEditFormOpen] = useState(false); + const [companyGroupDeleteOpen, setCompanyGroupDeleteOpen] = useState(false); + const [companyGroupDeleteLoading, setCompanyGroupDeleteLoading] = useState(false); const [activeTab, setActiveTab] = useState('legal_entities'); const [gskData, setGskData] = useState([]); const [gszData, setGszData] = useState([]); const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup); const relatedGroupFilter = useAppSelector((state) => state.catalogue_jur.relatedGroupFilter); const rawRows = useAppSelector((state) => state.company_groups.raw_rows); + const companyGroupsLoading = useAppSelector((state) => state.company_groups.loading); + const isGettingArchiveCompany = useAppSelector((state) => state.company_groups.isGettingArchive); + const visibleGroups = useMemo(() => visibleCompanyGroups(rawRows, isGettingArchiveCompany), [rawRows, isGettingArchiveCompany]); async function createJurHandler(values: CatalogueJurFormProps) { await createCatalogueJur(values); @@ -63,12 +77,43 @@ export function CatalogueGroup() { setKindForm(null); } } - async function createCompanyGroupHandler(values: CreateCompanyGroupProps) { + async function createCompanyGroupHandler(values: CompanyGroupFormProps) { await createCompanyGroup(values); dispatch(getCompanyGroupsThunk()); - getRelatedGroupsHandler(); setKindForm(null); } + async function editCompanyGroupHandler(values: CompanyGroupFormProps) { + if (activeCompanyGroup?.id) { + await editCompanyGroup(activeCompanyGroup?.id, values); + dispatch(getCompanyGroupsThunk()); + 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()); + + setCompanyGroupDeleteOpen(false); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ + description: e.message, + }); + } + } finally { + setCompanyGroupDeleteLoading(false); + } + } async function getRelatedGroupsHandler() { if (activeCompanyGroup?.id) { @@ -93,12 +138,18 @@ export function CatalogueGroup() { getRelatedGroupsHandler(); }, [activeCompanyGroup]); - // Без выбранной ГК таблица ЮЛ не грузится — выбираем первую ГК списка + // Без выбранной ГК таблица ЮЛ не грузится — выбираем первую ГК показанного списка; выбранная, ушедшая в архив + // (или из архива) или скрытая переключением «Архива», заменяется первой показанной, пустой список — без выбора useEffect(() => { - if (!activeCompanyGroup && rawRows?.length) { - dispatch(setActiveCompanyGroup(rawRows[0])); + // список ГК ещё не загружен — выбор не трогаем (при возврате на экран остаётся прежняя ГК) + if (companyGroupsLoading || rawRows.length === 0) { + return; } - }, [rawRows, activeCompanyGroup, dispatch]); + const next = nextActiveCompanyGroup(activeCompanyGroup, visibleGroups); + if (next?.id !== activeCompanyGroup?.id) { + dispatch(setActiveCompanyGroup(next)); + } + }, [visibleGroups, activeCompanyGroup, companyGroupsLoading, rawRows.length, dispatch]); useEffect(() => { setActiveTab('legal_entities'); @@ -140,8 +191,13 @@ export function CatalogueGroup() { flexDirection: 'column', overflow: 'hidden', }}> - - {activeTab === 'legal_entities' && ( + {!activeCompanyGroup && ( + + )} + {activeCompanyGroup && ( + + )} + {activeCompanyGroup && activeTab === 'legal_entities' && ( )} - {(activeTab === 'gsk' || activeTab === 'gsz') && ( + {activeCompanyGroup && (activeTab === 'gsk' || activeTab === 'gsz') && ( )} + {companyGroupEditFormOpen && activeCompanyGroup && ( + { + setCompanyGroupEditFormOpen(false); + }} + onOk={editCompanyGroupHandler} + companyGroupId={activeCompanyGroup.id} + /> + )} + {companyGroupDeleteOpen && activeCompanyGroup && ( + setCompanyGroupDeleteOpen(false)} + onOk={archiveCompanyGroupHandler} + isArchived={activeCompanyGroup.is_archived} + /> + )} ); } diff --git a/web/src/services/api/directories/company_groups.ts b/web/src/services/api/directories/company_groups.ts index 9ca3f26..3943d56 100644 --- a/web/src/services/api/directories/company_groups.ts +++ b/web/src/services/api/directories/company_groups.ts @@ -1,11 +1,11 @@ import { api } from '../../../api/client'; 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; export const LE_PAGE = 200; -/** Все видимые ГК — дочитываем страницы, пока не получим total. */ +/** Все видимые ГК, в том числе архивные (`is_archived`), — дочитываем страницы, пока не получим total. */ export async function fetchCompanyGroups(): Promise { const all: ICompanyGroupOut[] = []; for (let offset = 0; ; offset += CG_PAGE) { @@ -37,6 +37,15 @@ export async function fetchAllLegalEntities(groupId: string): Promise api.get(`/directories/legal-entities/${id}`); -export function createCompanyGroup(body: CreateCompanyGroupProps): Promise { +export function createCompanyGroup(body: CompanyGroupFormProps): Promise { return api.post('/directories/company-groups', body); } +export function editCompanyGroup(id: string, body: CompanyGroupFormProps): Promise { + return api.patch(`/directories/company-groups/${id}`, body); +} +export function archiveCompanyGroup(id: string): Promise { + return api.post(`/directories/company-groups/${id}/archive`); +} +export function unarchiveCompanyGroup(id: string): Promise { + return api.post(`/directories/company-groups/${id}/unarchive`); +} diff --git a/web/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts b/web/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts index c56a483..eb5fe97 100644 --- a/web/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts +++ b/web/src/store/slices/companyGroupsSlice/companyGroupsSlice.ts @@ -1,4 +1,4 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { type PayloadAction, createSlice } from '@reduxjs/toolkit'; import { getCompanyGroupsThunk } from './thunks'; import type { CompanyGroupSliceType } from './types'; @@ -6,13 +6,18 @@ const initialState: CompanyGroupSliceType = { raw_rows: [], total_groups: 0, loading: false, + isGettingArchive: false, error: null, }; const comGroupsSlice = createSlice({ name: 'company_groups', initialState, - reducers: {}, + reducers: { + setIsGettingArchive(state, action: PayloadAction) { + state.isGettingArchive = action.payload; + }, + }, extraReducers: (builder) => { builder .addCase(getCompanyGroupsThunk.pending, (state) => { @@ -30,4 +35,6 @@ const comGroupsSlice = createSlice({ }); }, }); +export const { setIsGettingArchive } = comGroupsSlice.actions; + export default comGroupsSlice.reducer; diff --git a/web/src/store/slices/companyGroupsSlice/selectors.test.ts b/web/src/store/slices/companyGroupsSlice/selectors.test.ts new file mode 100644 index 0000000..b4404b1 --- /dev/null +++ b/web/src/store/slices/companyGroupsSlice/selectors.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import type { ICompanyGroup } from '../../../types/companyGroups/companyGroups'; +import { nextActiveCompanyGroup, visibleCompanyGroups } from './selectors'; + +const group = (id: string, is_archived: boolean): ICompanyGroup => ({ + id, + name: id, + crm_id: null, + base_regional_office_id: null, + base_regional_office: null, + legal_entities_count: 0, + is_archived, + base_regional_office_name: '—', +}); + +const active1 = group('a1', false); +const active2 = group('a2', false); +const archived1 = group('z1', true); +const rows = [active1, archived1, active2]; + +describe('visibleCompanyGroups', () => { + it('без «Архива» — только действующие ГК, с «Архивом» — только архивные', () => { + expect(visibleCompanyGroups(rows, false)).toEqual([active1, active2]); + expect(visibleCompanyGroups(rows, true)).toEqual([archived1]); + }); +}); + +describe('nextActiveCompanyGroup', () => { + it('выбранная ГК в показанном списке остаётся', () => { + expect(nextActiveCompanyGroup(active2, [active1, active2])).toBe(active2); + }); + + it('без выбранной ГК берётся первая показанная', () => { + expect(nextActiveCompanyGroup(null, [active1, active2])).toBe(active1); + }); + + it('ГК ушла в архив — выбирается первая действующая', () => { + expect(nextActiveCompanyGroup({ ...active1, is_archived: true }, [active2])).toBe(active2); + }); + + it('пустой список — ГК не выбрана', () => { + expect(nextActiveCompanyGroup(active1, [])).toBeNull(); + expect(nextActiveCompanyGroup(null, [])).toBeNull(); + }); +}); diff --git a/web/src/store/slices/companyGroupsSlice/selectors.ts b/web/src/store/slices/companyGroupsSlice/selectors.ts new file mode 100644 index 0000000..574133d --- /dev/null +++ b/web/src/store/slices/companyGroupsSlice/selectors.ts @@ -0,0 +1,17 @@ +import type { ICompanyGroup } from '../../../types/companyGroups/companyGroups'; + +/** ГК списка справочника: действующие или, при включённом «Архиве», только архивные. */ +export function visibleCompanyGroups(rows: ICompanyGroup[], archived: boolean): ICompanyGroup[] { + return rows.filter((group) => group.is_archived === archived); +} + +/** + * Какая ГК должна быть выбрана в справочнике: выбранная — пока она в показанном списке; иначе (ушла в архив, вернулась + * из архива, скрыта переключением «Архива») — первая показанная, а в пустом списке — никакая. + */ +export function nextActiveCompanyGroup(active: ICompanyGroup | null | undefined, visible: ICompanyGroup[]): ICompanyGroup | null { + if (active && visible.some((group) => group.id === active.id)) { + return active; + } + return visible[0] ?? null; +} diff --git a/web/src/store/slices/companyGroupsSlice/types.ts b/web/src/store/slices/companyGroupsSlice/types.ts index 5f35cfa..589f61a 100644 --- a/web/src/store/slices/companyGroupsSlice/types.ts +++ b/web/src/store/slices/companyGroupsSlice/types.ts @@ -5,4 +5,6 @@ export interface CompanyGroupSliceType { raw_rows: ICompanyGroup[]; loading: boolean; error: string | null; + /** Список справочника показывает архивные ГК вместо действующих; `raw_rows` при этом не меняется. */ + isGettingArchive: boolean; } diff --git a/web/src/types/catalogueJur/catalogueJur.test.ts b/web/src/types/catalogueJur/catalogueJur.test.ts new file mode 100644 index 0000000..3560c65 --- /dev/null +++ b/web/src/types/catalogueJur/catalogueJur.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { type ICatalogueJur, type MemberShips, toCatalogueJurRow } from './catalogueJur'; + +const member = (kind: MemberShips['kind']): MemberShips => ({ + related_group_id: kind, + code: kind, + kind, + name: kind, + valid_from: new Date('2026-01-01'), +}); + +const row = (memberships?: MemberShips[]): ICatalogueJur => + ({ + id: 'le-1', + inn: '7701000040', + entity_type: 'ЮЛ', + name: 'ООО «Тест»', + company_group_name: null, + closed: false, + regional_office: { id: 'ro-1', name: 'Ростовский РФ', is_head_office: false }, + memberships, + }) as ICatalogueJur; + +describe('toCatalogueJurRow', () => { + it('имя РФ — плоским полем для поиска и фильтра', () => { + expect(toCatalogueJurRow(row()).regional_name).toBe('Ростовский РФ'); + }); + + it('признаки ГСК и ГСЗ — по видам членства', () => { + expect(toCatalogueJurRow(row([member('gsk')]))).toMatchObject({ in_gsk: true, in_gsz: false }); + expect(toCatalogueJurRow(row([member('gsz')]))).toMatchObject({ in_gsk: false, in_gsz: true }); + expect(toCatalogueJurRow(row())).toMatchObject({ in_gsk: false, in_gsz: false }); + }); +}); diff --git a/web/src/types/catalogueJur/catalogueJur.ts b/web/src/types/catalogueJur/catalogueJur.ts index 5d6e472..f992f7c 100644 --- a/web/src/types/catalogueJur/catalogueJur.ts +++ b/web/src/types/catalogueJur/catalogueJur.ts @@ -21,8 +21,19 @@ export interface ICatalogueJur { name: string; is_head_office: boolean; }; + /** Плоские поля строки таблицы (`toCatalogueJurRow`): по ним работают поиск, фильтры и колонки GenericTable. */ + regional_name?: string; + in_gsk?: boolean; + in_gsz?: boolean; } +export const toCatalogueJurRow = (row: ICatalogueJur): ICatalogueJur => ({ + ...row, + regional_name: row.regional_office?.name, + in_gsk: row.memberships?.some((member) => member.kind === 'gsk') ?? false, + in_gsz: row.memberships?.some((member) => member.kind === 'gsz') ?? false, +}); + export type KindType = 'gsk' | 'gsz'; export type MemberShips = { diff --git a/web/src/types/companyGroups/companyGroups.ts b/web/src/types/companyGroups/companyGroups.ts index 8909640..4def26d 100644 --- a/web/src/types/companyGroups/companyGroups.ts +++ b/web/src/types/companyGroups/companyGroups.ts @@ -6,6 +6,7 @@ export interface ICompanyGroupOut { base_regional_office_id: string | null; base_regional_office: { id: string; name: string; is_head_office: boolean } | null; legal_entities_count: number; + is_archived: boolean; } /** Строка таблицы ГК: имя филиала плоским полем — по нему работают поиск и фильтры GenericTable. */ @@ -18,7 +19,7 @@ export const toCompanyGroupRow = (g: ICompanyGroupOut): ICompanyGroup => ({ base_regional_office_name: g.base_regional_office?.name ?? '—', }); -export interface CreateCompanyGroupProps { +export interface CompanyGroupFormProps { name: string; crm_id: string; base_regional_office_id: string | null; diff --git a/web/src/widgets/ArchiveCompanyGroupModal/ArchiveCompanyGroupModal.tsx b/web/src/widgets/ArchiveCompanyGroupModal/ArchiveCompanyGroupModal.tsx new file mode 100644 index 0000000..65db289 --- /dev/null +++ b/web/src/widgets/ArchiveCompanyGroupModal/ArchiveCompanyGroupModal.tsx @@ -0,0 +1,28 @@ +import { Modal } from 'antd'; + +type Props = { + open: boolean; + onCancel: () => void; + onOk: () => void | Promise; + loading?: boolean; + isArchived: boolean; +}; + +export const ArchiveCompanyGroupModal: React.FC = ({ open, onCancel, onOk, loading, isArchived }) => { + return ( + +

+ {isArchived ? 'Группа компаний вернётся из архива в список действующих.' : 'Группа компаний будет перенесена в архив.'} +

+
+ ); +}; diff --git a/web/src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx b/web/src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx index 1b5deb2..a73b56f 100644 --- a/web/src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx +++ b/web/src/widgets/CatalogueGroupHeader/CatalogueGroupHeader.tsx @@ -1,33 +1,74 @@ -import { Space } from 'antd'; +import { CheckCircleOutlined, DeleteOutlined, EditOutlined, EllipsisOutlined, HistoryOutlined } from '@ant-design/icons'; +import { Button, Dropdown, Space } from 'antd'; import Text from 'antd/es/typography/Text'; import Title from 'antd/es/typography/Title'; +import { useNavigate } from 'react-router-dom'; import { useAppSelector } from '../../store/store'; import { pluralRu } from '../../utils/labels'; -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 count = activeCompanyGroup?.legal_entities_count ?? 0; - + const navigate = useNavigate(); const organizationWord = pluralRu(count, ['организация', 'организации', 'организаций']); + const items = [ + { + key: 'edit', + icon: , + label: 'Изменить ГК', + onClick: () => { + setEditFormOpen(true); + }, + }, + { + key: 'history', + icon: , + label: 'История изменений', + onClick: () => navigate('/logging'), + }, + { + key: 'delete', + icon: activeCompanyGroup?.is_archived ? : , + label: activeCompanyGroup?.is_archived ? 'Разархивировать' : 'Архивировать', + danger: !activeCompanyGroup?.is_archived, + onClick: () => { + setDeleteConfirmOpen(true); + }, + }, + ]; + return ( -
- - {activeCompanyGroup?.name ?? ''} - - - - {activeCompanyGroup?.base_regional_office_name ?? ''} - - - {count ?? 0} {organizationWord} - - -
+
+ + {activeCompanyGroup?.name ?? ''} + + + + {activeCompanyGroup?.base_regional_office_name ?? ''} + + + {count ?? 0} {organizationWord} + + +
+ + + + ); }; diff --git a/web/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx b/web/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx index 94c3112..8f24b8b 100644 --- a/web/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx +++ b/web/src/widgets/CatalogueJurTable/CatalogueJurTable.tsx @@ -19,18 +19,19 @@ import { setIsGettingArchive, setRelatedGroupFilter } from '../../store/slices/c import { getCatalogueJurThunk } from '../../store/slices/catalogueJurSlice/thunks'; import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks'; import { useAppDispatch, useAppSelector } from '../../store/store'; -import type { - CatalogueJurFormProps, - ICatalogueJur, - MoveCatalogueJurProps, - WithdrawCatalogueJurProps, +import { + type CatalogueJurFormProps, + type ICatalogueJur, + type MoveCatalogueJurProps, + type WithdrawCatalogueJurProps, + toCatalogueJurRow, } from '../../types/catalogueJur/catalogueJur'; import { pluralRu } from '../../utils/labels'; import { useNotificationApi } from '../../utils/useNotification'; import { CatalogueJurForm } from '../CatalogueJurForm/CatalogueJurForm'; import { MoveCatalogueJurForm } from '../MoveCatalogueJurForm/MoveCatalogueJurForm'; import { WithdrawCatalogueJurForm } from '../WithdrawCatalogueJurForm/WithdrawCatalogueJurForm'; -import { catalogueJurConfig } from './config'; +import { catalogueJurConfig, companyGroupJurConfig } from './config'; interface CatalogueJurTableI { outside_group: boolean; @@ -52,21 +53,23 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur const [editCatalogueFormOpen, setEditCatalogueFormOpen] = useState(false); const [withdrawCatalogueFormOpen, setWithdrawCatalogueFormOpen] = useState(false); const [jurId, setJurId] = useState(''); + const tableConfig = outside_group ? catalogueJurConfig : companyGroupJurConfig; const filterdData = useMemo(() => { + let newData = rawRows; 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 rawRows; + return newData?.map((elem) => elem && toCatalogueJurRow(elem)); }, [rawRows, relatedGroupFilter, outside_group]); - const { visibleKeys, setVisibleKeys } = useVisibleColumns(catalogueJurConfig); + const { visibleKeys, setVisibleKeys } = useVisibleColumns(tableConfig); const { rawQuery, setRawQuery, columnFilters, setColumnFilters, sort, setSort, resetAll, hasActiveFilters } = useTableFilters(); // single filtered array — title count and table share it - const { rows } = useFilteredRows(filterdData, catalogueJurConfig, rawQuery, columnFilters, sort); + const { rows } = useFilteredRows(filterdData, tableConfig, rawQuery, columnFilters, sort); function getCatalogueJur() { dispatch( @@ -233,7 +236,7 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur dispatch(setIsGettingArchive(e.target.checked))}> Архив - + {relatedGroupFilter?.id && !outside_group && ( @@ -281,7 +284,7 @@ export function CatalogueJurTable({ outside_group, tabsComponent }: CatalogueJur = { rowKey: 'id', - searchableKeys: ['closed', 'inn', 'kpp', 'name', 'entity_type'], + searchableKeys: ['closed', 'inn', 'crm_id', 'kpp', 'name', 'entity_type', 'regional_name'], columns: [ { key: 'closed', @@ -43,9 +44,29 @@ export const catalogueJurConfig: TableConfig = { }, }, { - key: 'regional_office', + key: 'regional_name', title: 'Основной РФ', - render: (val) => (val as ICatalogueJur['regional_office'] | null)?.name, + features: { search: true, multiselect: true, sort: true }, }, ], }; + +const membershipColumn = (key: 'in_gsk' | 'in_gsz', title: string) => ({ + key, + title, + render: (value: unknown) => { + const preset = value ? TAG_PRESETS.green : TAG_PRESETS.gray; + return ; + }, +}); + +/** ЮЛ карточки ГК: после статуса — признаки «Входит в ГСК» / «Входит в ГСЗ» (поля строки из `toCatalogueJurRow`). */ +export const companyGroupJurConfig: TableConfig = { + ...catalogueJurConfig, + columns: [ + catalogueJurConfig.columns[0], + membershipColumn('in_gsk', 'Входит в ГСК'), + membershipColumn('in_gsz', 'Входит в ГСЗ'), + ...catalogueJurConfig.columns.slice(1), + ], +}; diff --git a/web/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx b/web/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx index 76e31f6..a922677 100644 --- a/web/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx +++ b/web/src/widgets/CompanyGroupForm/CompanyGroupForm.tsx @@ -3,26 +3,47 @@ import type React from 'react'; import { useEffect, useState } from 'react'; import { ApiError } from '../../api/client'; import { useMe } from '../../auth/useMe'; -import { useAppSelector } from '../../store/store'; -import type { CreateCompanyGroupProps } from '../../types/companyGroups/companyGroups'; +import { fetchOneCompanyGroup } from '../../services/api/directories/company_groups'; +import type { CompanyGroupFormProps } from '../../types/companyGroups/companyGroups'; import { useNotificationApi } from '../../utils/useNotification'; import { loadOffices } from '../Admin/UsersTable'; type Props = { open: boolean; onCancel: () => void; - onOk: (values: CreateCompanyGroupProps) => void | Promise; + onOk: (values: CompanyGroupFormProps) => void | Promise; + companyGroupId?: string; }; -export const CompanyGroupForm: React.FC = ({ open, onCancel, onOk }) => { - const [form] = Form.useForm(); +export const CompanyGroupForm: React.FC = ({ open, onCancel, onOk, companyGroupId }) => { + const [form] = Form.useForm(); const { me } = useMe(); const notification = useNotificationApi(); - const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup); const [officesOptions, setOfficesOptions] = useState<{ value: string; label: string }[]>([]); const [loadingOptions, setLoadingOptions] = 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() { setLoadingOptions(true); @@ -45,6 +66,7 @@ export const CompanyGroupForm: React.FC = ({ open, onCancel, onOk }) => { const values = await form.validateFields(); await onOk({ ...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), }); } catch (e) { @@ -72,18 +94,20 @@ export const CompanyGroupForm: React.FC = ({ open, onCancel, onOk }) => { }; useEffect(() => { + getInitialData(); getOfficesOptionsHandler(); - }, [activeCompanyGroup]); + }, []); return (
diff --git a/web/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx b/web/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx index 47d0caf..51767f1 100644 --- a/web/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx +++ b/web/src/widgets/CompanyGroupsList/CompanyGroupsList.tsx @@ -1,8 +1,10 @@ import { FolderOpenOutlined, SearchOutlined } from '@ant-design/icons'; -import { Empty, Input, List, Spin, Tag, Typography } from 'antd'; +import { Checkbox, Empty, Input, List, Spin, Tag, Typography } from 'antd'; import { useEffect, useMemo, useState } from 'react'; import { useVirtualList } from '../../hooks/useVirtualList'; import { setActiveCompanyGroup } from '../../store/slices/catalogueJurSlice/catalogueJurSlice'; +import { setIsGettingArchive } from '../../store/slices/companyGroupsSlice/companyGroupsSlice'; +import { visibleCompanyGroups } from '../../store/slices/companyGroupsSlice/selectors'; import { getCompanyGroupsThunk } from '../../store/slices/companyGroupsSlice/thunks'; import { useAppDispatch, useAppSelector } from '../../store/store'; @@ -14,14 +16,15 @@ export const CompanyGroupsList = () => { const rawRows = useAppSelector((state) => state.company_groups.raw_rows); const loading = useAppSelector((state) => state.company_groups.loading); const activeCompanyGroup = useAppSelector((state) => state.catalogue_jur.activeCompanyGroup); - + const isGettingArchiveCompany = useAppSelector((state) => state.company_groups.isGettingArchive); const [query, setQuery] = useState(''); const filtered = useMemo(() => { + const rows = visibleCompanyGroups(rawRows, isGettingArchiveCompany); 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]); + if (!q) return rows; + return rows.filter((g) => g.name.toLowerCase().includes(q) || g.crm_id?.toLowerCase().includes(q)); + }, [rawRows, isGettingArchiveCompany, query]); const { ref, onScroll, visibleItems, totalHeight, offsetY } = useVirtualList(filtered, { itemHeight: 56, @@ -33,12 +36,6 @@ export const CompanyGroupsList = () => { dispatch(getCompanyGroupsThunk()); }, [dispatch]); - useEffect(() => { - if (!activeCompanyGroup && rawRows?.length) { - dispatch(setActiveCompanyGroup(rawRows?.[0])); - } - }, [rawRows, activeCompanyGroup, dispatch]); - if (loading) { return (
@@ -57,14 +54,18 @@ export const CompanyGroupsList = () => { borderRight: '1px solid #f0f0f0', borderRadius: 12, }}> -
+
} placeholder='Поиск по ГК или CRM ID' value={query} onChange={(e) => setQuery(e.target.value)} + style={{ flex: 1 }} /> + dispatch(setIsGettingArchive(e.target.checked))}> + Архив +
diff --git a/web/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx b/web/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx index 61ed5c5..9561a59 100644 --- a/web/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx +++ b/web/src/widgets/MoveCatalogueJurForm/MoveCatalogueJurForm.tsx @@ -109,7 +109,7 @@ export const MoveCatalogueJurForm: React.FC = ({ open, onCancel, onOk, ju showSearch allowClear options={raw_rows - .filter((row) => row.id !== form.getFieldValue('old_company_group_id')) + .filter((row) => !row.is_archived && row.id !== form.getFieldValue('old_company_group_id')) .map((g) => ({ value: g.id, label: g.name }))} notFoundContent={loading ? 'Загрузка' : 'Нет вариантов'} optionFilterProp='label' diff --git a/web/src/widgets/NewConsoleModal/NewConsoleModal.tsx b/web/src/widgets/NewConsoleModal/NewConsoleModal.tsx index 80882ec..b95d0e0 100644 --- a/web/src/widgets/NewConsoleModal/NewConsoleModal.tsx +++ b/web/src/widgets/NewConsoleModal/NewConsoleModal.tsx @@ -167,7 +167,7 @@ export const NewConsolidationModal: React.FC = ({ open, onCancel, onOk }) showSearch allowClear filterOption={(input, option) => (option?.label as string)?.toLowerCase().includes(input.toLowerCase())} - options={raw_rows.map((g) => ({ value: g.id, label: g.name }))} + options={raw_rows.filter((g) => !g.is_archived).map((g) => ({ value: g.id, label: g.name }))} onChange={handleGroupChange} /> diff --git a/web/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx b/web/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx index d4ed42e..ce8a9b5 100644 --- a/web/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx +++ b/web/src/widgets/RelatedGroupForm/RelatedGroupForm.tsx @@ -39,6 +39,7 @@ export const RelatedGroupForm: React.FC = ({ open, onCancel, onOk, relate try { const data = await fetchRelatedGroup(relatedGroupId); await dispatch(getCompanyGroupsThunk()); + form.setFieldsValue({ crm_id: data?.crm_id, name: data?.name, diff --git a/web/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx b/web/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx index f3a842a..d5819a4 100644 --- a/web/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx +++ b/web/src/widgets/RelatedGroupTable/RelatedGroupTable.tsx @@ -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 type { ColumnsType } from 'antd/es/table'; import { type ReactNode, useMemo, useState } from 'react'; @@ -77,6 +77,7 @@ export function RelatedGroupTable({ tabsComponent, data, loading, getData, activ const items = [ { key: 'edit', + icon: , label: 'Изменить', onClick: () => { setGroupId(row.id); @@ -85,6 +86,7 @@ export function RelatedGroupTable({ tabsComponent, data, loading, getData, activ }, { key: 'delete', + icon: , label: 'Удалить', danger: true, onClick: () => {