fix(web): список консолидаций и «Взять в работу» по спеке (AURORA-1589, PR #166)
«Только в работе» выключен по умолчанию, параметр — boolean; ошибка загрузки списка показывается; пункты «⋯» disabled с подсказкой «Будет позже», клик не выбирает строку; «Взять в работу» неактивна по коду CANCELLED. Модалка [35]: опечатки, плейсхолдер, ≤ 1000 символов, «К3», ошибки (403 и прочие) — текст сервера, ФИО без второй точки; после взятия в работу перечитываются список и панель «Организации» («Ответственный»). Боковое меню: thunk списка с объектом, отступ секций. Порядок импортов (biome). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ccca6dfa13
commit
d71c46e846
@ -1,6 +1,6 @@
|
|||||||
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 { Settings2 } from 'lucide-react';
|
||||||
import { type ColumnDef, isGroup } from './tableTypes';
|
import { type ColumnDef, isGroup } from './tableTypes';
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
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';
|
||||||
|
import { Filter } from 'lucide-react';
|
||||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { CustomFilterDropdown } from './CustomFilterDropdown';
|
import { CustomFilterDropdown } from './CustomFilterDropdown';
|
||||||
import { Highlight } from './Highlight';
|
import { Highlight } from './Highlight';
|
||||||
|
|||||||
@ -5,16 +5,14 @@ import { ApiError } from '../api/client';
|
|||||||
import PageTitle from '../components/PageTitle';
|
import PageTitle from '../components/PageTitle';
|
||||||
import { fetchOneCompanyGroup } from '../services/api/directories/company_groups';
|
import { fetchOneCompanyGroup } from '../services/api/directories/company_groups';
|
||||||
import { setSelectedVersion } from '../store/slices/consolidations/consolidationsSlice';
|
import { setSelectedVersion } from '../store/slices/consolidations/consolidationsSlice';
|
||||||
import { getConsolidationsListThunk } from '../store/slices/consolidations/thunks';
|
|
||||||
import { useAppDispatch } from '../store/store';
|
import { useAppDispatch } from '../store/store';
|
||||||
import { ConsoleLegalEntitiesTable } from '../widgets/ConsoleLegalEntities/ConsoleLegalEntities';
|
import { ConsoleLegalEntitiesTable } from '../widgets/ConsoleLegalEntities/ConsoleLegalEntities';
|
||||||
import { ConsoleTableWidget } from '../widgets/ConsoleTable/ConsoleTable';
|
import { ConsoleTableWidget } from '../widgets/ConsoleTable/ConsoleTable';
|
||||||
import { DocumentsDrawer } from '../widgets/FilesDrawer/FilesDrawer';
|
import { DocumentsDrawer } from '../widgets/FilesDrawer/FilesDrawer';
|
||||||
|
|
||||||
// unwrap() бросает rejectValue (строку), fetch группы — ApiError
|
// список консолидаций грузит ConsoleTableWidget (с фильтром «Только в работе»), здесь — только группа
|
||||||
function errorText(e: unknown): string {
|
function errorText(e: unknown): string {
|
||||||
if (e instanceof ApiError) return e.message;
|
if (e instanceof ApiError) return e.message;
|
||||||
if (typeof e === 'string') return e;
|
|
||||||
return 'Не удалось загрузить группу компаний';
|
return 'Не удалось загрузить группу компаний';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +41,7 @@ export function ConsolidationPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
getAllInfo(group_id);
|
getAllInfo(group_id);
|
||||||
}, [dispatch, group_id]);
|
}, [group_id]);
|
||||||
|
|
||||||
// выбор версии следует за ?version= и не перезагружает список
|
// выбор версии следует за ?version= и не перезагружает список
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -85,7 +83,8 @@ export function ConsolidationPage() {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
}}>
|
}}>
|
||||||
<ConsoleTableWidget />
|
{/* после «Взять в работу» перечитываем и панель «Организации»: в ней «Ответственный» */}
|
||||||
|
<ConsoleTableWidget onVersionLocked={() => setReloadKey((k) => k + 1)} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -1,85 +1,63 @@
|
|||||||
import { DownOutlined, RightOutlined } from '@ant-design/icons';
|
import { DownOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import { Button, Checkbox, Dropdown, Space, Tooltip, message } from 'antd';
|
import { Button, Checkbox, Dropdown, type MenuProps, Space, Tooltip, message } from 'antd';
|
||||||
import type { ColumnsType, TableProps } from 'antd/es/table';
|
import type { ColumnsType, TableProps } from 'antd/es/table';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { ArrowRight, Briefcase, GitBranchPlus, History, MoreHorizontal, ToggleLeft } from 'lucide-react';
|
||||||
|
import { type ReactNode, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
import { useCurrentUser } from '../../auth/MeContext';
|
||||||
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 { getConsolidationsListThunk } from '../../store/slices/consolidations/thunks';
|
||||||
import { useAppDispatch, 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 { canTakeInWork, isLockedBy, parseInWorkOnly } from '../../utils/consolidations';
|
||||||
|
import LockModal from './LockModal';
|
||||||
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';
|
const SOON = 'Будет позже';
|
||||||
const ITEMS = [
|
|
||||||
{
|
|
||||||
key: 'copy',
|
|
||||||
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;
|
|
||||||
|
|
||||||
const RowActions = memo(function RowActions({ row, onAction }: { row: ConsRow; onAction: (key: ActionKey, _row: ConsRow) => void }) {
|
// пункт неактивен (disabled — клик не обрабатывается), подсказка — на подписи
|
||||||
|
const soonItem = (key: string, label: string, icon: ReactNode) => ({
|
||||||
|
key,
|
||||||
|
disabled: true,
|
||||||
|
icon,
|
||||||
|
label: (
|
||||||
|
<Tooltip title={SOON}>
|
||||||
|
<span style={{ display: 'block', width: '100%' }}>{label}</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// функций ещё нет: «История изменений» — после журнала с фильтрами [43-02], остальное — зонтичная задача экранов консолидаций
|
||||||
|
const ITEMS: MenuProps['items'] = [
|
||||||
|
soonItem('copy', 'Добавить версию', <GitBranchPlus size={14} />),
|
||||||
|
soonItem('disable_confirm', 'Отключить подтверждение КФО', <ToggleLeft size={14} />),
|
||||||
|
soonItem('history', 'История изменений', <History size={14} />),
|
||||||
|
];
|
||||||
|
|
||||||
|
const RowActions = memo(function RowActions() {
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
// меню рендерится в портал, но React-событие всплывает по дереву до onRow строки — гасим здесь
|
||||||
trigger={['click']}
|
<span onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
|
||||||
menu={{
|
<Dropdown trigger={['click']} menu={{ items: ITEMS }}>
|
||||||
items: ITEMS as any,
|
<Button icon={<MoreHorizontal size={14} />} aria-label='Действия' />
|
||||||
onClick: ({ key, domEvent }) => {
|
</Dropdown>
|
||||||
domEvent.stopPropagation();
|
</span>
|
||||||
onAction(key as ActionKey, row);
|
|
||||||
},
|
|
||||||
}}>
|
|
||||||
<Button icon={<MoreHorizontal size={14} />} onClick={(e) => e.stopPropagation()} />
|
|
||||||
</Dropdown>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
function isLockedByCurrentUser(currentUser: Me, lockData: LockOut | null) {
|
|
||||||
if (lockData === null) return false;
|
/** `onVersionLocked` — версию взяли в работу: страница перечитывает панель «Организации» («Ответственный»). */
|
||||||
if (lockData.locked_by !== currentUser.id) return false;
|
export function ConsoleTableWidget({ onVersionLocked }: { onVersionLocked?: () => void }) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
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;
|
// «Только в работе» по умолчанию выключен (макет [34], умолчание API — false)
|
||||||
|
const inWorkOnly = parseInWorkOnly(searchParams.get('in_work_only'));
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -96,11 +74,12 @@ export function ConsoleTableWidget() {
|
|||||||
const { visibleRows, expanded, toggle } = useGrouping(grouped);
|
const { visibleRows, expanded, toggle } = useGrouping(grouped);
|
||||||
const loadConsolidations = useCallback(async () => {
|
const loadConsolidations = useCallback(async () => {
|
||||||
if (!group_id) return;
|
if (!group_id) return;
|
||||||
let in_work_only = true;
|
try {
|
||||||
if (inWorkOnly === 'false') {
|
await dispatch(getConsolidationsListThunk({ company_group_id: group_id, in_work_only: inWorkOnly })).unwrap();
|
||||||
in_work_only = false;
|
} catch (e: unknown) {
|
||||||
|
// unwrap() бросает rejectValue — текст ошибки сервера
|
||||||
|
message.error(typeof e === 'string' ? e : 'Не удалось загрузить консолидации');
|
||||||
}
|
}
|
||||||
await dispatch(getConsolidationsListThunk({ company_group_id: group_id, in_work_only })).unwrap();
|
|
||||||
}, [dispatch, group_id, inWorkOnly]);
|
}, [dispatch, group_id, inWorkOnly]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadConsolidations();
|
loadConsolidations();
|
||||||
@ -152,16 +131,6 @@ export function ConsoleTableWidget() {
|
|||||||
|
|
||||||
const { visibleKeys, setVisibleKeys } = useVisibleColumns(config);
|
const { visibleKeys, setVisibleKeys } = useVisibleColumns(config);
|
||||||
|
|
||||||
const handleDropdownAction = useCallback(
|
|
||||||
(key: ActionKey, _row: ConsRow) => {
|
|
||||||
if (key === 'history') {
|
|
||||||
navigate('/logging');
|
|
||||||
} else {
|
|
||||||
message.info('Будет позже');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[navigate],
|
|
||||||
);
|
|
||||||
const extraColumns: ColumnsType<ConsRow> = useMemo(
|
const extraColumns: ColumnsType<ConsRow> = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -170,7 +139,7 @@ export function ConsoleTableWidget() {
|
|||||||
width: 120,
|
width: 120,
|
||||||
render: (_: unknown, row: ConsRow) => (
|
render: (_: unknown, row: ConsRow) => (
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
{isLockedByCurrentUser(currentUser, row.lock) ? (
|
{isLockedBy(row.lock, currentUser.id) ? (
|
||||||
<Button disabled icon={<Briefcase size={16} />} type='default'>
|
<Button disabled icon={<Briefcase size={16} />} type='default'>
|
||||||
В работе
|
В работе
|
||||||
</Button>
|
</Button>
|
||||||
@ -179,7 +148,7 @@ export function ConsoleTableWidget() {
|
|||||||
iconPlacement='start'
|
iconPlacement='start'
|
||||||
icon={<Briefcase size={16} />}
|
icon={<Briefcase size={16} />}
|
||||||
type='default'
|
type='default'
|
||||||
disabled={row.status === 'Отменён'}
|
disabled={!canTakeInWork(row.status_code)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setLockVersionId(row.id);
|
setLockVersionId(row.id);
|
||||||
@ -198,13 +167,17 @@ export function ConsoleTableWidget() {
|
|||||||
}}>
|
}}>
|
||||||
К отчётности
|
К отчётности
|
||||||
</Button>
|
</Button>
|
||||||
<RowActions row={row} onAction={handleDropdownAction} />
|
<RowActions />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[navigate, group_id, handleDropdownAction, currentUser],
|
[navigate, group_id, currentUser],
|
||||||
);
|
);
|
||||||
|
const afterLock = useCallback(() => {
|
||||||
|
loadConsolidations();
|
||||||
|
onVersionLocked?.();
|
||||||
|
}, [loadConsolidations, onVersionLocked]);
|
||||||
|
|
||||||
const onRow: TableProps<ConsRow>['onRow'] = (row) => ({
|
const onRow: TableProps<ConsRow>['onRow'] = (row) => ({
|
||||||
// выбранная версия живёт в URL (?version=): переживает уход на «К отчётности» и «Назад»
|
// выбранная версия живёт в URL (?version=): переживает уход на «К отчётности» и «Назад»
|
||||||
@ -252,7 +225,7 @@ export function ConsoleTableWidget() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={searchParams.get('in_work_only') === 'false' ? false : true}
|
checked={inWorkOnly}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchParams(
|
setSearchParams(
|
||||||
(searchParams) => {
|
(searchParams) => {
|
||||||
@ -284,7 +257,7 @@ export function ConsoleTableWidget() {
|
|||||||
open={lockModalOpen}
|
open={lockModalOpen}
|
||||||
versionId={lockVersionId}
|
versionId={lockVersionId}
|
||||||
onOpenChange={setLockModalOpen}
|
onOpenChange={setLockModalOpen}
|
||||||
handleReload={loadConsolidations}
|
handleReload={afterLock}
|
||||||
currentUserId={currentUser.id}
|
currentUserId={currentUser.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
import { Alert, Button, Input, Modal, Skeleton, message } from 'antd';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
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 { ApiError } from '../../api/client';
|
||||||
|
import { useBeforeUnload } from '../../hooks/useBeforeUnload';
|
||||||
import { getVersionCard, lockVersion } from '../../services/api/consolidations';
|
import { getVersionCard, lockVersion } from '../../services/api/consolidations';
|
||||||
|
import type { LockOut } from '../../types/consolidations/common_types';
|
||||||
|
import { MAX_REASON_LENGTH, endSentence, isValidReason } from '../../utils/consolidations';
|
||||||
|
import { quarterShortLabel } from '../../utils/labels';
|
||||||
|
|
||||||
type LockModalProps = {
|
type LockModalProps = {
|
||||||
versionId: string | null;
|
versionId: string | null;
|
||||||
@ -11,12 +14,8 @@ type LockModalProps = {
|
|||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
handleReload: () => void;
|
handleReload: () => void;
|
||||||
};
|
};
|
||||||
function isValidReason(reason: string) {
|
|
||||||
const trimmed = reason.trim();
|
|
||||||
if (trimmed.length > 0 && trimmed.length < 1000) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** «Взять консолидацию в работу?» [35]: свободная версия — подтверждение, чужая — перехват с причиной. */
|
||||||
export default function LockModal({ versionId, open, onOpenChange, currentUserId, handleReload }: LockModalProps) {
|
export default function LockModal({ versionId, open, onOpenChange, currentUserId, handleReload }: LockModalProps) {
|
||||||
const [data, setData] = useState<LockOut | null>(null);
|
const [data, setData] = useState<LockOut | null>(null);
|
||||||
const [consolidationName, setConsolidationName] = useState<string | null>(null);
|
const [consolidationName, setConsolidationName] = useState<string | null>(null);
|
||||||
@ -27,11 +26,14 @@ export default function LockModal({ versionId, open, onOpenChange, currentUserId
|
|||||||
const [conflict, setConflict] = useState(false);
|
const [conflict, setConflict] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// Stale-response guard for fast open/switch/close sequences.
|
// Защита от устаревшего ответа при быстром открытии/смене версии/закрытии.
|
||||||
const reqId = useRef(0);
|
const reqId = useRef(0);
|
||||||
|
|
||||||
const close = useCallback(() => onOpenChange(false), [onOpenChange]);
|
const close = useCallback(() => onOpenChange(false), [onOpenChange]);
|
||||||
|
|
||||||
|
// введённая причина перехвата — несохранённая правка: браузер предупредит при закрытии вкладки
|
||||||
|
useBeforeUnload(open && reason.trim() !== '');
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!versionId) return;
|
if (!versionId) return;
|
||||||
const my = ++reqId.current;
|
const my = ++reqId.current;
|
||||||
@ -39,16 +41,14 @@ export default function LockModal({ versionId, open, onOpenChange, currentUserId
|
|||||||
setLoadError(false);
|
setLoadError(false);
|
||||||
try {
|
try {
|
||||||
const currentCard = await getVersionCard(versionId);
|
const currentCard = await getVersionCard(versionId);
|
||||||
const lockData = currentCard.lock;
|
|
||||||
const { company_group, year, quarter } = currentCard;
|
const { company_group, year, quarter } = currentCard;
|
||||||
const consName = `${year} ${quarter} · ${company_group.name}`;
|
if (my !== reqId.current) return; // успел прийти ответ на более новый запрос
|
||||||
if (my !== reqId.current) return; // a newer request won
|
setData(currentCard.lock);
|
||||||
setData(lockData);
|
setConsolidationName(`${year} ${quarterShortLabel(quarter)} · ${company_group.name}`);
|
||||||
setConsolidationName(consName);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (my !== reqId.current) return;
|
if (my !== reqId.current) return;
|
||||||
if (e instanceof ApiError && e.status === 404) {
|
if (e instanceof ApiError && e.status === 404) {
|
||||||
message.warning('Версия консолидации не обнаржуена');
|
message.warning('Версия консолидации не обнаружена');
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -58,46 +58,47 @@ export default function LockModal({ versionId, open, onOpenChange, currentUserId
|
|||||||
}
|
}
|
||||||
}, [versionId, close]);
|
}, [versionId, close]);
|
||||||
|
|
||||||
// Reset + fetch on every open, and on target change while open.
|
// Сброс и загрузка при каждом открытии и при смене версии в открытой модалке (load меняется вместе с versionId).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
const requests = reqId; // счётчик запросов, а не DOM-узел: в cleanup нужен его актуальный .current
|
||||||
setReason('');
|
setReason('');
|
||||||
setConflict(false);
|
setConflict(false);
|
||||||
setData(null);
|
setData(null);
|
||||||
setLoadError(false);
|
setLoadError(false);
|
||||||
load();
|
load();
|
||||||
return () => {
|
return () => {
|
||||||
reqId.current++; // invalidate in-flight response on close/switch
|
requests.current++; // ответ в полёте больше не нужен
|
||||||
};
|
};
|
||||||
}, [open, versionId, load]);
|
}, [open, load]);
|
||||||
|
|
||||||
const hasToReason = !loading && data !== null && data.locked_by !== currentUserId;
|
const hasToReason = !loading && data !== null && data.locked_by !== currentUserId;
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
if (!versionId || loading || submitting) return;
|
if (!versionId || loading || submitting) return;
|
||||||
if (hasToReason && !reason.trim()) return;
|
if (hasToReason && !isValidReason(reason)) return;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const result = await lockVersion(versionId, hasToReason ? reason.trim() : null);
|
await lockVersion(versionId, hasToReason ? reason.trim() : null);
|
||||||
|
|
||||||
close();
|
close();
|
||||||
message.success('Консолидация взята в работу.');
|
message.success('Консолидация взята в работу.');
|
||||||
|
handleReload();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ApiError && e.status === 409) {
|
if (e instanceof ApiError && e.code === 'VERSION_NOT_EDITABLE') {
|
||||||
if (e.code === 'VERSION_NOT_EDITABLE') {
|
message.error('Нельзя взять в работу отменённую версию.');
|
||||||
message.error('Нельзя взять в работу отменённую версию.');
|
close();
|
||||||
close();
|
handleReload();
|
||||||
} else if (e.code === 'LOCKED_BY_OTHER' && !conflict) {
|
} else if (e instanceof ApiError && e.code === 'LOCKED_BY_OTHER' && !conflict) {
|
||||||
await load();
|
// версию взяли, пока модалка была открыта, — режим перехвата
|
||||||
setConflict(true);
|
await load();
|
||||||
setReason('');
|
setConflict(true);
|
||||||
}
|
setReason('');
|
||||||
} else {
|
} else {
|
||||||
message.error('Что-то пошло не так. Попробуйте снова.');
|
// 403 без права, прочие 409/422 — текст сервера
|
||||||
|
message.error(e instanceof ApiError ? e.message : 'Не удалось взять консолидацию в работу');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
handleReload();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -110,6 +111,7 @@ export default function LockModal({ versionId, open, onOpenChange, currentUserId
|
|||||||
closable={!submitting}
|
closable={!submitting}
|
||||||
okText='Взять в работу'
|
okText='Взять в работу'
|
||||||
okButtonProps={{ disabled: loading || loadError || (hasToReason && !isValidReason(reason)) }}
|
okButtonProps={{ disabled: loading || loadError || (hasToReason && !isValidReason(reason)) }}
|
||||||
|
confirmLoading={submitting}
|
||||||
cancelText='Отмена'
|
cancelText='Отмена'
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
cancelButtonProps={{ disabled: submitting }}>
|
cancelButtonProps={{ disabled: submitting }}>
|
||||||
@ -119,10 +121,10 @@ export default function LockModal({ versionId, open, onOpenChange, currentUserId
|
|||||||
<Alert
|
<Alert
|
||||||
type='error'
|
type='error'
|
||||||
showIcon
|
showIcon
|
||||||
title='Не получилось загрузить нформацию о текущей блокировке'
|
title='Не удалось загрузить информацию о текущей блокировке'
|
||||||
action={
|
action={
|
||||||
<Button size='small' onClick={load}>
|
<Button size='small' onClick={load}>
|
||||||
Попробовать перезагрузить
|
Повторить
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -141,12 +143,13 @@ export default function LockModal({ versionId, open, onOpenChange, currentUserId
|
|||||||
|
|
||||||
{hasToReason && data ? (
|
{hasToReason && data ? (
|
||||||
<>
|
<>
|
||||||
<p style={{ marginTop: 0 }}>Консолидация взята в работу {data.locked_by_name}. Укажите причину замены.</p>
|
<p style={{ marginTop: 0 }}>Консолидация взята в работу {endSentence(data.locked_by_name)} Укажите причину замены.</p>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder='Type message'
|
placeholder='Укажите причину замены'
|
||||||
value={reason}
|
value={reason}
|
||||||
maxLength={1000}
|
maxLength={MAX_REASON_LENGTH}
|
||||||
|
showCount
|
||||||
rows={3}
|
rows={3}
|
||||||
onChange={(e) => setReason(e.target.value)}
|
onChange={(e) => setReason(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
|
import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import { Button, Divider, Layout, Tooltip, message } from 'antd';
|
import { Button, Divider, Layout, Tooltip, message } from 'antd';
|
||||||
|
import { Archive, Building2, ScrollText, UserCog2, Users2 } from 'lucide-react';
|
||||||
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';
|
||||||
import { USERS_ADMIN } from '../../auth/permissions';
|
import { USERS_ADMIN } from '../../auth/permissions';
|
||||||
@ -10,7 +11,6 @@ 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;
|
||||||
@ -205,7 +205,8 @@ export function Sidebar() {
|
|||||||
onOk={async (values) => {
|
onOk={async (values) => {
|
||||||
const card = await createConsolidation(values);
|
const card = await createConsolidation(values);
|
||||||
// перечитываем список: на странице группы он сам не обновится, и новая консолидация не станет текущей
|
// перечитываем список: на странице группы он сам не обновится, и новая консолидация не станет текущей
|
||||||
await dispatch(getConsolidationsListThunk(values.company_group_id));
|
// in_work_only: false — переход ниже открывает список без ?in_work_only (флажок выключен)
|
||||||
|
await dispatch(getConsolidationsListThunk({ company_group_id: values.company_group_id, in_work_only: false }));
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
message.success(`Создана консолидация ${values.year}, ${quarterLabel(values.quarter)}`);
|
message.success(`Создана консолидация ${values.year}, ${quarterLabel(values.quarter)}`);
|
||||||
navigate(`/groups/${values.company_group_id}?version=${card.id}`);
|
navigate(`/groups/${values.company_group_id}?version=${card.id}`);
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
/* padding: 12px; */
|
/* отступ сверху и снизу, как в макете [34]; по горизонтали пункты выровнены по кнопке «Новая консолидация» */
|
||||||
|
padding: 12px 0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user