fix: блокировка версии, доработка визуала таблицы версий консолидации, фильтр 'Только в работе'
This commit is contained in:
parent
2e75de33cc
commit
5ee54bd94f
@ -15,6 +15,7 @@
|
|||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@reduxjs/toolkit": "^2.8.2",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"antd": "^6.5.0",
|
"antd": "^6.5.0",
|
||||||
|
"lucide-react": "0.263.1",
|
||||||
"markdown-to-jsx": "^9.1.2",
|
"markdown-to-jsx": "^9.1.2",
|
||||||
"pdfjs-dist": "^5.7.284",
|
"pdfjs-dist": "^5.7.284",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { SettingOutlined } from '@ant-design/icons';
|
import { Settings2 } from 'lucide-react';
|
||||||
import { Button, Checkbox, Dropdown } from 'antd';
|
import { Button, Checkbox, Dropdown } from 'antd';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
import { type ColumnDef, isGroup } from './tableTypes';
|
import { type ColumnDef, isGroup } from './tableTypes';
|
||||||
@ -41,7 +41,7 @@ export function ColumnSettings<T>({ defs, visibleKeys, onChange }: Props<T>) {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={{ items }} trigger={['click']} placement='bottomRight'>
|
<Dropdown menu={{ items }} trigger={['click']} placement='bottomRight'>
|
||||||
<Button icon={<SettingOutlined />}>Настройка таблицы</Button>
|
<Button icon={<Settings2 size={14} />}>Настройка таблицы</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FilterOutlined } from '@ant-design/icons';
|
import { Filter } from 'lucide-react';
|
||||||
import { Table } from 'antd';
|
import { Table } from 'antd';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
import type { ColumnGroupType, ColumnType, ColumnsType } from 'antd/es/table';
|
import type { ColumnGroupType, ColumnType, ColumnsType } from 'antd/es/table';
|
||||||
@ -94,7 +94,7 @@ export function DataTable<T extends Record<string, any>>({
|
|||||||
color: active ? 'rgb(22, 101, 52)' : '#bfbfbf',
|
color: active ? 'rgb(22, 101, 52)' : '#bfbfbf',
|
||||||
transition: 'background 0.15s',
|
transition: 'background 0.15s',
|
||||||
}}>
|
}}>
|
||||||
<FilterOutlined style={{ fontSize: 12 }} />
|
<Filter size={12} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -38,7 +38,6 @@ export function ConsolidationPage() {
|
|||||||
setComGroupName(data.name);
|
setComGroupName(data.name);
|
||||||
setLocationPath(`Отчётность>${data.name}>Консолидации`);
|
setLocationPath(`Отчётность>${data.name}>Консолидации`);
|
||||||
setDesc(`${data.legal_entities_count} организаций`);
|
setDesc(`${data.legal_entities_count} организаций`);
|
||||||
await dispatch(getConsolidationsListThunk(id)).unwrap();
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
message.error(errorText(e));
|
message.error(errorText(e));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,17 @@ import type { LockOut } from '../../types/consolidations/common_types';
|
|||||||
import type { IConsolidation } from '../../types/consolidations/consolidations';
|
import type { IConsolidation } from '../../types/consolidations/consolidations';
|
||||||
import type { ChecklistBackendShape, VersionCard } from '../../types/consolidations/version_members';
|
import type { ChecklistBackendShape, VersionCard } from '../../types/consolidations/version_members';
|
||||||
|
|
||||||
export const getConsolidations = (groupId: string) => api.get<IConsolidation[]>(`/company-groups/${groupId}/consolidations`);
|
export const getConsolidations = (groupId: string, in_work_only: boolean) =>
|
||||||
|
api.get<IConsolidation[]>(`/company-groups/${groupId}/consolidations?in_work_only=${in_work_only}`);
|
||||||
export const getVersionCard = (versionId: string) => api.get<VersionCard>(`/consolidations/versions/${versionId}`);
|
export const getVersionCard = (versionId: string) => api.get<VersionCard>(`/consolidations/versions/${versionId}`);
|
||||||
export const createConsolidation = (body: ConsolidationCreate) => api.post<VersionCard>('/consolidations', body);
|
export const createConsolidation = (body: ConsolidationCreate) => api.post<VersionCard>('/consolidations', body);
|
||||||
export const downloadChecklistCsv = (versionId: string) => apiFetchBlob(`/consolidations/versions/${versionId}/checklist?format=csv`);
|
export const downloadChecklistCsv = (versionId: string) => apiFetchBlob(`/consolidations/versions/${versionId}/checklist?format=csv`);
|
||||||
export const getChecklist = (versionId: string) => api.get<ChecklistBackendShape>(`/consolidations/versions/${versionId}/checklist`);
|
export const getChecklist = (versionId: string) => api.get<ChecklistBackendShape>(`/consolidations/versions/${versionId}/checklist`);
|
||||||
/** «Взять в работу»: 201 — взята, 200 — уже ваша; чужая — 409 `LOCKED_BY_OTHER` (перехват — с `takeover_reason`). */
|
/** «Взять в работу»: 201 — взята, 200 — уже ваша; чужая — 409 `LOCKED_BY_OTHER` (перехват — с `takeover_reason`). */
|
||||||
export const lockVersion = (versionId: string) => api.post<LockOut>(`/consolidations/versions/${versionId}/lock`);
|
export const lockVersion = (versionId: string, reason?: string | null) => {
|
||||||
|
const body = {};
|
||||||
|
if (reason) {
|
||||||
|
body['takeover_reason'] = reason;
|
||||||
|
}
|
||||||
|
return api.post<LockOut>(`/consolidations/versions/${versionId}/lock`, body);
|
||||||
|
};
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
|||||||
import { getConsolidations } from '../../../services/api/consolidations';
|
import { getConsolidations } from '../../../services/api/consolidations';
|
||||||
import type { IConsolidation } from '../../../types/consolidations/consolidations';
|
import type { IConsolidation } from '../../../types/consolidations/consolidations';
|
||||||
|
|
||||||
export const getConsolidationsListThunk = createAsyncThunk<IConsolidation[], string, { rejectValue: string }>(
|
export const getConsolidationsListThunk = createAsyncThunk<
|
||||||
'consolidations/getByCompanyGroup',
|
IConsolidation[],
|
||||||
async (company_group_id, thunkApi) => {
|
{ company_group_id: string; in_work_only: boolean },
|
||||||
try {
|
{ rejectValue: string }
|
||||||
return await getConsolidations(company_group_id);
|
>('consolidations/getByCompanyGroup', async ({ company_group_id, in_work_only = false }, thunkApi) => {
|
||||||
} catch (e) {
|
try {
|
||||||
return thunkApi.rejectWithValue(e instanceof Error ? e.message : 'Ошибка');
|
return await getConsolidations(company_group_id, in_work_only);
|
||||||
}
|
} catch (e) {
|
||||||
},
|
return thunkApi.rejectWithValue(e instanceof Error ? e.message : 'Ошибка');
|
||||||
);
|
}
|
||||||
|
});
|
||||||
|
|||||||
@ -1,22 +1,55 @@
|
|||||||
import { ArrowRightOutlined, DownOutlined, MoreOutlined, RightOutlined } from '@ant-design/icons';
|
import { DownOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import { Button, Dropdown, Space, message } from 'antd';
|
import { Button, Checkbox, Dropdown, Space, Tooltip, message } from 'antd';
|
||||||
import type { ColumnsType, TableProps } from 'antd/es/table';
|
import type { ColumnsType, TableProps } from 'antd/es/table';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { ColumnSettings, DataTable, type TableConfig, isGroup, useFilteredRows, useVisibleColumns } from '../../components/GenericTable';
|
import { ColumnSettings, DataTable, type TableConfig, isGroup, useFilteredRows, useVisibleColumns } from '../../components/GenericTable';
|
||||||
import { useTableFilters } from '../../components/GenericTable/useTableFilters';
|
import { useTableFilters } from '../../components/GenericTable/useTableFilters';
|
||||||
import { useAppSelector } from '../../store/store';
|
import { useAppDispatch, useAppSelector } from '../../store/store';
|
||||||
import type { ConsRow } from '../../types/consolidations/consolidations';
|
import type { ConsRow } from '../../types/consolidations/consolidations';
|
||||||
import { consConfigBase } from './config';
|
import { consConfigBase } from './config';
|
||||||
import { flattenCons } from './flatten';
|
import { flattenCons } from './flatten';
|
||||||
import { groupAdjacent } from './groupAdjacent';
|
import { groupAdjacent } from './groupAdjacent';
|
||||||
import { useGrouping } from './useGrouping';
|
import { useGrouping } from './useGrouping';
|
||||||
|
import { ArrowRight, Briefcase, GitBranchPlus, History, MoreHorizontal, ToggleLeft } from 'lucide-react';
|
||||||
|
import { useCurrentUser } from '../../auth/MeContext';
|
||||||
|
import LockModal from './LockModal';
|
||||||
|
import { LockOut } from '../../types/consolidations/common_types';
|
||||||
|
import { Me } from '../../api/types';
|
||||||
|
import { getConsolidationsListThunk } from '../../store/slices/consolidations/thunks';
|
||||||
|
|
||||||
type ActionKey = 'copy' | 'disable_confirm' | 'history';
|
type ActionKey = 'copy' | 'disable_confirm' | 'history';
|
||||||
const ITEMS = [
|
const ITEMS = [
|
||||||
{ key: 'copy', label: 'Добавить версию' },
|
{
|
||||||
{ key: 'disable_confirm', label: 'Отключить подвтерждение КФО' },
|
key: 'copy',
|
||||||
{ key: 'history', label: 'История изменений' },
|
label: (
|
||||||
|
<Tooltip title='Будет позже'>
|
||||||
|
<span style={{ display: 'block', width: '100%', cursor: 'not-allowed' }}>Добавить версию</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
className: 'ant-dropdown-menu-item-disabled',
|
||||||
|
icon: <GitBranchPlus size={14} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disable_confirm',
|
||||||
|
label: (
|
||||||
|
<Tooltip title='Будет позже'>
|
||||||
|
<span style={{ display: 'block', width: '100%', cursor: 'not-allowed' }}>Отключить подтверждение КФО</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
className: 'ant-dropdown-menu-item-disabled',
|
||||||
|
icon: <ToggleLeft size={14} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'history',
|
||||||
|
label: (
|
||||||
|
<Tooltip title='Будет позже'>
|
||||||
|
<span style={{ display: 'block', width: '100%', cursor: 'not-allowed' }}>История изменений</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
className: 'ant-dropdown-menu-item-disabled',
|
||||||
|
icon: <History size={14} />,
|
||||||
|
},
|
||||||
{},
|
{},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@ -31,19 +64,29 @@ const RowActions = memo(function RowActions({ row, onAction }: { row: ConsRow; o
|
|||||||
onAction(key as ActionKey, row);
|
onAction(key as ActionKey, row);
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<Button icon={<MoreOutlined />} onClick={(e) => e.stopPropagation()} />
|
<Button icon={<MoreHorizontal size={14} />} onClick={(e) => e.stopPropagation()} />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
function isLockedByCurrentUser(currentUser: Me, lockData: LockOut | null) {
|
||||||
|
if (lockData === null) return false;
|
||||||
|
if (lockData.locked_by !== currentUser.id) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
export function ConsoleTableWidget() {
|
export function ConsoleTableWidget() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { group_id } = useParams();
|
const { group_id } = useParams();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const consolidations = useAppSelector((s) => s.consolidations.consolidations);
|
const consolidations = useAppSelector((s) => s.consolidations.consolidations);
|
||||||
const loading = useAppSelector((s) => s.consolidations.loading);
|
const loading = useAppSelector((s) => s.consolidations.loading);
|
||||||
|
const inWorkOnly = searchParams.get('in_work_only') || true;
|
||||||
|
const currentUser = useCurrentUser();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { columnFilters, setColumnFilters, sort, setSort, resetAll, hasActiveFilters } = useTableFilters<ConsRow>();
|
const { columnFilters, setColumnFilters, sort, setSort, resetAll, hasActiveFilters } = useTableFilters<ConsRow>();
|
||||||
const rawRows = useMemo(() => flattenCons(consolidations), [consolidations]);
|
const rawRows = useMemo(() => flattenCons(consolidations), [consolidations]);
|
||||||
|
const [lockModalOpen, setLockModalOpen] = useState<boolean>(false);
|
||||||
|
const [lockVersionId, setLockVersionId] = useState<string | null>(null);
|
||||||
|
|
||||||
// const [sort, setSort] = useState<SortState<ConsRow>>(null);
|
// const [sort, setSort] = useState<SortState<ConsRow>>(null);
|
||||||
|
|
||||||
@ -51,6 +94,17 @@ export function ConsoleTableWidget() {
|
|||||||
const { rows: filtered } = useFilteredRows(rawRows, consConfigBase, '', columnFilters, sort);
|
const { rows: filtered } = useFilteredRows(rawRows, consConfigBase, '', columnFilters, sort);
|
||||||
const grouped = useMemo(() => groupAdjacent(filtered), [filtered]);
|
const grouped = useMemo(() => groupAdjacent(filtered), [filtered]);
|
||||||
const { visibleRows, expanded, toggle } = useGrouping(grouped);
|
const { visibleRows, expanded, toggle } = useGrouping(grouped);
|
||||||
|
const loadConsolidations = useCallback(async () => {
|
||||||
|
if (!group_id) return;
|
||||||
|
let in_work_only = true;
|
||||||
|
if (inWorkOnly === 'false') {
|
||||||
|
in_work_only = false;
|
||||||
|
}
|
||||||
|
await dispatch(getConsolidationsListThunk({ company_group_id: group_id, in_work_only })).unwrap();
|
||||||
|
}, [dispatch, group_id, inWorkOnly]);
|
||||||
|
useEffect(() => {
|
||||||
|
loadConsolidations();
|
||||||
|
}, [loadConsolidations]);
|
||||||
|
|
||||||
const config: TableConfig<ConsRow> = useMemo(
|
const config: TableConfig<ConsRow> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -115,22 +169,41 @@ export function ConsoleTableWidget() {
|
|||||||
title: '',
|
title: '',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (_: unknown, row: ConsRow) => (
|
render: (_: unknown, row: ConsRow) => (
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
{isLockedByCurrentUser(currentUser, row.lock) ? (
|
||||||
|
<Button disabled icon={<Briefcase size={16} />} type='default'>
|
||||||
|
В работе
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
iconPlacement='start'
|
||||||
|
icon={<Briefcase size={16} />}
|
||||||
|
type='default'
|
||||||
|
disabled={row.status === 'Отменён'}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setLockVersionId(row.id);
|
||||||
|
setLockModalOpen(true);
|
||||||
|
}}>
|
||||||
|
Взять в работу
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type='default'
|
type='default'
|
||||||
|
icon={<ArrowRight size={16} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// иначе клик всплывает до onRow строки, и его setSearchParams(replace) отменяет переход
|
// иначе клик всплывает до onRow строки, и его setSearchParams(replace) отменяет переход
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate(`/groups/${group_id}/versions/${row.id}/osv`);
|
navigate(`/groups/${group_id}/versions/${row.id}/osv`);
|
||||||
}}>
|
}}>
|
||||||
К отчётности <ArrowRightOutlined />
|
К отчётности
|
||||||
</Button>
|
</Button>
|
||||||
<RowActions row={row} onAction={handleDropdownAction} />
|
<RowActions row={row} onAction={handleDropdownAction} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[navigate, group_id, handleDropdownAction],
|
[navigate, group_id, handleDropdownAction, currentUser],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onRow: TableProps<ConsRow>['onRow'] = (row) => ({
|
const onRow: TableProps<ConsRow>['onRow'] = (row) => ({
|
||||||
@ -171,6 +244,19 @@ export function ConsoleTableWidget() {
|
|||||||
Сбросить фильтры
|
Сбросить фильтры
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Checkbox
|
||||||
|
checked={searchParams.get('in_work_only') === 'false' ? false : true}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchParams(
|
||||||
|
(searchParams) => {
|
||||||
|
searchParams.set('in_work_only', String(e.target.checked));
|
||||||
|
return searchParams;
|
||||||
|
},
|
||||||
|
{ replace: true },
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
Только в работе
|
||||||
|
</Checkbox>
|
||||||
<ColumnSettings defs={config.columns} visibleKeys={visibleKeys} onChange={setVisibleKeys} />
|
<ColumnSettings defs={config.columns} visibleKeys={visibleKeys} onChange={setVisibleKeys} />
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
@ -187,6 +273,13 @@ export function ConsoleTableWidget() {
|
|||||||
extraColumns={extraColumns}
|
extraColumns={extraColumns}
|
||||||
onRow={onRow}
|
onRow={onRow}
|
||||||
/>
|
/>
|
||||||
|
<LockModal
|
||||||
|
open={lockModalOpen}
|
||||||
|
versionId={lockVersionId}
|
||||||
|
onOpenChange={setLockModalOpen}
|
||||||
|
handleReload={loadConsolidations}
|
||||||
|
currentUserId={currentUser.id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
161
web/src/widgets/ConsoleTable/LockModal.tsx
Normal file
161
web/src/widgets/ConsoleTable/LockModal.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { LockOut } from '../../types/consolidations/common_types';
|
||||||
|
import { Alert, Button, Input, message, Modal, Skeleton } from 'antd';
|
||||||
|
import { ApiError } from '../../api/client';
|
||||||
|
import { getVersionCard, lockVersion } from '../../services/api/consolidations';
|
||||||
|
|
||||||
|
type LockModalProps = {
|
||||||
|
versionId: string | null;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
currentUserId: string;
|
||||||
|
handleReload: () => void;
|
||||||
|
};
|
||||||
|
function isValidReason(reason: string) {
|
||||||
|
const trimmed = reason.trim();
|
||||||
|
if (trimmed.length > 0 && trimmed.length < 1000) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LockModal({ versionId, open, onOpenChange, currentUserId, handleReload }: LockModalProps) {
|
||||||
|
const [data, setData] = useState<LockOut | null>(null);
|
||||||
|
const [consolidationName, setConsolidationName] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadError, setLoadError] = useState(false);
|
||||||
|
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
const [conflict, setConflict] = useState(false);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Stale-response guard for fast open/switch/close sequences.
|
||||||
|
const reqId = useRef(0);
|
||||||
|
|
||||||
|
const close = useCallback(() => onOpenChange(false), [onOpenChange]);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
if (!versionId) return;
|
||||||
|
const my = ++reqId.current;
|
||||||
|
setLoading(true);
|
||||||
|
setLoadError(false);
|
||||||
|
try {
|
||||||
|
const currentCard = await getVersionCard(versionId);
|
||||||
|
const lockData = currentCard.lock;
|
||||||
|
const { company_group, year, quarter } = currentCard;
|
||||||
|
const consName = `${year} ${quarter} · ${company_group.name}`;
|
||||||
|
if (my !== reqId.current) return; // a newer request won
|
||||||
|
setData(lockData);
|
||||||
|
setConsolidationName(consName);
|
||||||
|
} catch (e) {
|
||||||
|
if (my !== reqId.current) return;
|
||||||
|
if (e instanceof ApiError && e.status === 404) {
|
||||||
|
message.warning('Версия консолидации не обнаржуена');
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadError(true);
|
||||||
|
} finally {
|
||||||
|
if (my === reqId.current) setLoading(false);
|
||||||
|
}
|
||||||
|
}, [versionId, close]);
|
||||||
|
|
||||||
|
// Reset + fetch on every open, and on target change while open.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setReason('');
|
||||||
|
setConflict(false);
|
||||||
|
setData(null);
|
||||||
|
setLoadError(false);
|
||||||
|
load();
|
||||||
|
return () => {
|
||||||
|
reqId.current++; // invalidate in-flight response on close/switch
|
||||||
|
};
|
||||||
|
}, [open, versionId, load]);
|
||||||
|
|
||||||
|
const hasToReason = !loading && data !== null && data.locked_by !== currentUserId;
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
if (!versionId || loading || submitting) return;
|
||||||
|
if (hasToReason && !reason.trim()) return;
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const result = await lockVersion(versionId, hasToReason ? reason.trim() : null);
|
||||||
|
|
||||||
|
close();
|
||||||
|
message.success('Консолидация взята в работу.');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ApiError && e.status === 409) {
|
||||||
|
if (e.code === 'VERSION_NOT_EDITABLE') {
|
||||||
|
message.error('Нельзя взять в работу отменённую версию.');
|
||||||
|
close();
|
||||||
|
} else if (e.code === 'LOCKED_BY_OTHER' && !conflict) {
|
||||||
|
await load();
|
||||||
|
setConflict(true);
|
||||||
|
setReason('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error('Что-то пошло не так. Попробуйте снова.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
handleReload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title='Взять консолидацию в работу?'
|
||||||
|
onCancel={() => !submitting && close()}
|
||||||
|
centered
|
||||||
|
closable={!submitting}
|
||||||
|
okText='Взять в работу'
|
||||||
|
okButtonProps={{ disabled: loading || loadError || (hasToReason && !isValidReason(reason)) }}
|
||||||
|
cancelText='Отмена'
|
||||||
|
onOk={handleOk}
|
||||||
|
cancelButtonProps={{ disabled: submitting }}>
|
||||||
|
{loading && <Skeleton active paragraph={{ rows: 2 }} />}
|
||||||
|
|
||||||
|
{!loading && loadError && (
|
||||||
|
<Alert
|
||||||
|
type='error'
|
||||||
|
showIcon
|
||||||
|
title='Не получилось загрузить нформацию о текущей блокировке'
|
||||||
|
action={
|
||||||
|
<Button size='small' onClick={load}>
|
||||||
|
Попробовать перезагрузить
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !loadError && (
|
||||||
|
<>
|
||||||
|
{conflict && hasToReason && data && (
|
||||||
|
<Alert
|
||||||
|
type='warning'
|
||||||
|
showIcon
|
||||||
|
title={`${data.locked_by_name} только что взял(-а) консолидацию в работу.`}
|
||||||
|
style={{ marginBottom: 12 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasToReason && data ? (
|
||||||
|
<>
|
||||||
|
<p style={{ marginTop: 0 }}>Консолидация взята в работу {data.locked_by_name}. Укажите причину замены.</p>
|
||||||
|
<Input.TextArea
|
||||||
|
autoFocus
|
||||||
|
placeholder='Type message'
|
||||||
|
value={reason}
|
||||||
|
maxLength={1000}
|
||||||
|
rows={3}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p style={{ marginBottom: 0 }}>Консолидация {consolidationName} будет закреплена за вами для редактирования.</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ export const consConfigBase: TableConfig<ConsRow> = {
|
|||||||
{ key: 'year', title: 'Год', features: { sort: true, multiselect: true } },
|
{ key: 'year', title: 'Год', features: { sort: true, multiselect: true } },
|
||||||
{ key: 'quarter', title: 'Квартал', features: { multiselect: true }, render: (v) => quarterShortLabel(v as QuarterEnum) },
|
{ key: 'quarter', title: 'Квартал', features: { multiselect: true }, render: (v) => quarterShortLabel(v as QuarterEnum) },
|
||||||
|
|
||||||
{ key: 'version_no', title: 'Версия', features: { sort: true } },
|
{ key: 'version_no', title: 'Версия', features: { sort: true }, render: (v) => (String(v).trim() !== '1' ? '' : v) },
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
title: 'Статус',
|
title: 'Статус',
|
||||||
@ -23,8 +23,8 @@ export const consConfigBase: TableConfig<ConsRow> = {
|
|||||||
render: (v) => <TableTag {...statusColorMapping[v as ConsRussianStatus]} text={v as string} />,
|
render: (v) => <TableTag {...statusColorMapping[v as ConsRussianStatus]} text={v as string} />,
|
||||||
},
|
},
|
||||||
// ГСК в версии не хранится (решение владельца П-3) — колонка как в макете [34], пустая
|
// ГСК в версии не хранится (решение владельца П-3) — колонка как в макете [34], пустая
|
||||||
{ key: 'gsk', title: 'ГСК', features: {}, render: () => '—' },
|
{ key: 'gsk', title: 'ГСК', features: { search: true, multiselect: true }, render: () => '—' },
|
||||||
{ key: 'created_at', title: 'Дата создания', features: { sort: true }, render: ruDate },
|
{ key: 'created_at', title: 'Дата создания', features: {}, render: ruDate },
|
||||||
// даты верификации в API нет (VersionBrief), до маршрута согласования (Срез 7) — прочерк
|
// даты верификации в API нет (VersionBrief), до маршрута согласования (Срез 7) — прочерк
|
||||||
{ key: 'verified_at', title: 'Дата верификации', features: {}, render: () => '—' },
|
{ key: 'verified_at', title: 'Дата верификации', features: {}, render: () => '—' },
|
||||||
{ key: 'members_count', title: 'Организаций', features: {} },
|
{ key: 'members_count', title: 'Организаций', features: {} },
|
||||||
|
|||||||
@ -1,14 +1,4 @@
|
|||||||
import {
|
import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
BankOutlined,
|
|
||||||
ContainerOutlined,
|
|
||||||
InboxOutlined,
|
|
||||||
LeftOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
RightOutlined,
|
|
||||||
SolutionOutlined,
|
|
||||||
TeamOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Button, Divider, Layout, Tooltip, message } from 'antd';
|
import { Button, Divider, Layout, Tooltip, message } from 'antd';
|
||||||
import { type ReactNode, memo, useState } from 'react';
|
import { type ReactNode, memo, useState } from 'react';
|
||||||
import { NavLink, useNavigate } from 'react-router-dom';
|
import { NavLink, useNavigate } from 'react-router-dom';
|
||||||
@ -20,6 +10,7 @@ import { useAppDispatch } from '../../store/store';
|
|||||||
import { quarterLabel } from '../../utils/labels';
|
import { quarterLabel } from '../../utils/labels';
|
||||||
import { NewConsolidationModal } from '../NewConsoleModal/NewConsoleModal';
|
import { NewConsolidationModal } from '../NewConsoleModal/NewConsoleModal';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
import { Archive, Building2, ScrollText, UserCog2, Users2 } from 'lucide-react';
|
||||||
|
|
||||||
type CollapsedIconProps = {
|
type CollapsedIconProps = {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
@ -51,7 +42,7 @@ function CollapsedIcon({ icon, onClick, title, to, cn = '' }: CollapsedIconProps
|
|||||||
type NavItem = {
|
type NavItem = {
|
||||||
link: string;
|
link: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: ReactNode;
|
icon?: ReactNode;
|
||||||
/** Право из `/me`, без которого пункт не показывается; нет — пункт виден всем. */
|
/** Право из `/me`, без которого пункт не показывается; нет — пункт виден всем. */
|
||||||
permission?: string;
|
permission?: string;
|
||||||
};
|
};
|
||||||
@ -69,12 +60,10 @@ const config: Group[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
link: '/groups',
|
link: '/groups',
|
||||||
icon: <BankOutlined />,
|
|
||||||
label: 'Группы компаний',
|
label: 'Группы компаний',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: '/groups/monozaemchiki',
|
link: '/groups/monozaemchiki',
|
||||||
icon: <UserOutlined />,
|
|
||||||
label: 'Монозаемщики',
|
label: 'Монозаемщики',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -85,12 +74,12 @@ const config: Group[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
link: '/catalogue/groups',
|
link: '/catalogue/groups',
|
||||||
icon: <TeamOutlined />,
|
icon: <Building2 size={14} />,
|
||||||
label: 'ГК и организации',
|
label: 'ГК и организации',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: '/catalogue/jur_l',
|
link: '/catalogue/jur_l',
|
||||||
icon: <SolutionOutlined />,
|
icon: <Users2 size={14} />,
|
||||||
label: 'ЮЛ вне ГК',
|
label: 'ЮЛ вне ГК',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -101,7 +90,7 @@ const config: Group[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
link: '/docstorage',
|
link: '/docstorage',
|
||||||
icon: <InboxOutlined />,
|
icon: <Archive size={14} />,
|
||||||
label: 'Единое хранилище',
|
label: 'Единое хранилище',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -112,12 +101,12 @@ const config: Group[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
link: '/logging',
|
link: '/logging',
|
||||||
icon: <ContainerOutlined />,
|
icon: <ScrollText size={14} />,
|
||||||
label: 'Журнал действий',
|
label: 'Журнал действий',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: '/admin',
|
link: '/admin',
|
||||||
icon: <TeamOutlined />,
|
icon: <UserCog2 size={14} />,
|
||||||
label: 'Пользователи и роли',
|
label: 'Пользователи и роли',
|
||||||
permission: USERS_ADMIN,
|
permission: USERS_ADMIN,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user