fix: карточка консолидации

This commit is contained in:
maxim 2026-10-22 19:11:38 +03:00
parent 5ee54bd94f
commit 723c84cc72
6 changed files with 169 additions and 8 deletions

View File

@ -1,6 +1,6 @@
import { DownloadOutlined, FilterOutlined } from '@ant-design/icons';
import { Alert, Button, Space, message } from 'antd';
import { useEffect, useState } from 'react';
import { Alert, Button, Space, Tooltip, message } from 'antd';
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ApiError } from '../api/client';
import PageTitle from '../components/PageTitle';
@ -20,13 +20,17 @@ import { OsvTable } from '../widgets/ConsolidatedOsv/OsvTable';
import { OsvTabs } from '../widgets/ConsolidatedOsv/OsvTabs';
import { mapEntryId } from '../widgets/ConsolidatedOsv/counterpartyMapping';
import { OSV_PAGE_SIZE, useConsolidatedOsv } from '../widgets/ConsolidatedOsv/useConsolidatedOsv';
import { useCurrentUser } from '../auth/MeContext';
import ChangeStatusModal from '../widgets/StatusChangeModal/ChangeStatusModal';
function OsvContent({ versionId }: { versionId: string }) {
const [card, setCard] = useState<VersionCard | null>(null);
const [statusModalOpen, setModalOpen] = useState(false);
const [exporting, setExporting] = useState(false);
// строка, для которой открыт выбор организации («Указать» или ИНН сопоставленного ЮЛ)
const [mapping, setMapping] = useState<ConsolidatedRowOut | null>(null);
const osv = useConsolidatedOsv(versionId);
const currentUser = useCurrentUser();
useEffect(() => {
let actual = true;
@ -70,9 +74,37 @@ function OsvContent({ versionId }: { versionId: string }) {
description={russianStatus}
extra={
card && (
<Space size={8}>
<Space size={8} style={{ display: 'flex', gap: 4 }}>
<span style={{ color: '#667085', fontSize: 12 }}>Статус отчёта</span>
<TableTag text={russianStatus} {...preset} />
<TableTag
style={{
padding: '3px 10px',
width: 158,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 11,
}}
text={russianStatus}
{...preset}
/>
{card.lock === null || (card.lock && card.lock.locked_by !== currentUser.id) ? (
<Tooltip title='Сначала возьмите версию в работу'>
<Button size='middle' style={{ fontSize: 12 }} type='default' disabled>
Изменить статус
</Button>
</Tooltip>
) : (
<Button
size='middle'
onClick={() => {
setModalOpen(true);
}}
style={{ fontSize: 12 }}
type='default'>
Изменить статус
</Button>
)}
</Space>
)
}
@ -144,6 +176,14 @@ function OsvContent({ versionId }: { versionId: string }) {
onMapped={osv.refresh}
/>
)}
<ChangeStatusModal
open={statusModalOpen}
versionId={versionId}
onSuccess={setCard}
setClose={() => {
setModalOpen(false);
}}
/>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { api, apiFetchBlob } from '../../api/client';
import type { ConsolidationCreate } from '../../types/api';
import type { ConsolidationCreate, VersionStatusUpdate } from '../../types/api';
import type { LockOut } from '../../types/consolidations/common_types';
import type { IConsolidation } from '../../types/consolidations/consolidations';
import type { ChecklistBackendShape, VersionCard } from '../../types/consolidations/version_members';
@ -18,3 +18,6 @@ export const lockVersion = (versionId: string, reason?: string | null) => {
}
return api.post<LockOut>(`/consolidations/versions/${versionId}/lock`, body);
};
export const updateStatus = (versionId: string, body: VersionStatusUpdate) =>
api.post<VersionCard>(`/consolidation/versions/${versionId}/status`, body);

View File

@ -1,5 +1,7 @@
// DTO API arm-core (/api/v1). Повторяют arm-core/api/src/schemas/; Decimal и даты — строки.
import { ConsStatus } from './consolidations/common_types';
export type Quarter = 'Q1' | 'Q2' | 'Q3' | 'Q4';
export type DocumentType = 'BALANCE' | 'INCOME' | 'OSV' | 'DISCLOSURE' | 'OTHER';
export type DocumentStatus = 'RECOGNIZED' | 'NEEDS_CHECK' | 'CONFIRMED' | 'ERROR';
@ -394,3 +396,8 @@ export interface LegalEntityBrief {
name: string;
inn: string;
}
export interface VersionStatusUpdate {
to_status: ConsStatus;
comment: string;
}

View File

@ -18,7 +18,7 @@ export type ConsRussianStatus =
| 'Верификация РФ'
| 'Инициация'
| 'Обработка документов'
| 'Коррекция данных'
| 'Корректировка данных'
| 'Консолидация ГО'
| 'Консолидация РФ'
| 'Отменён'

View File

@ -7,7 +7,7 @@ export const statusMapping: Record<ConsStatus, ConsRussianStatus> = {
VERIFICATION_RF: 'Верификация РФ',
INITIATED: 'Инициация',
DOCUMENTS_PROCESSING: 'Обработка документов',
DATA_CORRECTION: 'Коррекция данных',
DATA_CORRECTION: 'Корректировка данных',
CONSOLIDATION_GO: 'Консолидация ГО',
CONSOLIDATION_RF: 'Консолидация РФ',
CANCELLED: 'Отменён',
@ -25,7 +25,7 @@ export const statusColorMapping: Record<ConsRussianStatus, Preset> = {
'Консолидация ГО': TAG_PRESETS.purple,
'Консолидация РФ': TAG_PRESETS.purple,
Корректировка: TAG_PRESETS.gold,
'Коррекция данных': TAG_PRESETS.gold,
'Корректировка данных': TAG_PRESETS.gold,
'Обработка документов': TAG_PRESETS.gold,
Отклонён: TAG_PRESETS.red,
Отменён: TAG_PRESETS.red,

View File

@ -0,0 +1,111 @@
import { Form, Input, message, Modal, Select } from 'antd';
import { useEffect, useState } from 'react';
import { ConsStatus } from '../../types/consolidations/common_types';
import { VersionStatusUpdate } from '../../types/api';
import { getVersionCard, updateStatus } from '../../services/api/consolidations';
import { statusColorMapping, statusMapping } from '../ConsoleTable/consts';
import { TableTag } from '../../components/TableTag/TableTag';
import { TAG_PRESETS } from '../../components/TableTag/presets';
import { ApiError } from '../../api/client';
import { VersionCard } from '../../types/consolidations/version_members';
type StatusModalProps = {
open: boolean;
versionId: string;
setClose: () => void;
onSuccess: (card: VersionCard) => void;
};
const known409Codes = ['VERSION_NOT_EDITABLE', 'LOCK_REQUIRED', 'LOCKED_BY_OTHER', 'TRANSITION_NOT_ALLOWED'];
const errorCodeMap = {
VERSION_NOT_EDITABLE: 'Версию нельзя отредактировать',
LOCK_REQUIRED: 'Сначала возьмите версию в работу',
LOCKED_BY_OTHER: 'Версия находится в работе у другого пользователя',
TRANSITION_NOT_ALLOWED: 'Данный переход недоступен',
};
/*
"Инициация", "Обработка документов", "Корректировка данных" -> "Отменена" (комментарий обязателен);
из "Обработка документов", "Корректировка данных" -> "Инициация" (комментарий обязателен во фронте)
*/
async function mockStatusMappingFetch() {
const mapping: Record<ConsStatus, ConsStatus[]> = {
INITIATED: ['CANCELLED'],
DOCUMENTS_PROCESSING: ['CANCELLED', 'INITIATED'],
DATA_CORRECTION: ['CANCELLED', 'INITIATED'],
};
return mapping;
}
export default function ChangeStatusModal({ open, versionId, setClose, onSuccess }: StatusModalProps) {
const [currentStatus, setCurrentStatus] = useState<ConsStatus | null>(null);
const [nextStatusOptions, setNextStatusOptions] = useState<ConsStatus[]>([]);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm<VersionStatusUpdate>();
const nextStatus = Form.useWatch('to_status', form);
const comment = Form.useWatch('comment', form);
useEffect(() => {
async function getAllData() {
if (!versionId) return;
setLoading(true);
const versionCard = await getVersionCard(versionId);
const status = versionCard.status;
const allNextOptions = await mockStatusMappingFetch();
setCurrentStatus(status);
setNextStatusOptions(allNextOptions[status]);
setLoading(false);
}
getAllData();
}, [versionId]);
const russianStatus = currentStatus ? statusMapping[currentStatus] : '';
const badgePreset = russianStatus ? statusColorMapping[russianStatus] : TAG_PRESETS.default;
const isSubmitDisabled = !nextStatus || !comment.trim();
const handleOk = async () => {
const values = await form.validateFields();
try {
const newCard = await updateStatus(versionId, { ...values, comment: values.comment.trim() });
onSuccess(newCard);
setClose();
} catch (e) {
if (e instanceof ApiError && e.status === 409 && e.code) {
if (known409Codes.includes(e.code)) {
message.error(errorCodeMap[e.code]);
}
} else {
message.error('Непредвиденная ошибка');
}
}
};
return (
<Modal
open={open}
title='Изменить статус'
okText='Изменить статус'
cancelText='Отмена'
okButtonProps={{ disabled: isSubmitDisabled || loading }}
loading={loading}
onOk={handleOk}
onCancel={setClose}
destroyOnHidden>
<div style={{ display: 'flex' }}>
Текущий этап: <TableTag text={russianStatus} {...badgePreset} />
</div>
<Form form={form} layout='vertical' requiredMark={false} initialValues={{ comment: '', to_status: nextStatusOptions[0] }}>
<Form.Item name='to_status' label='Новый этап'>
<Select
options={nextStatusOptions.map((i) => ({ label: i, value: i }))}
labelRender={(v) => (v.value ? statusMapping[v.value as ConsStatus] : v.value)}
optionRender={(v) => (v.value ? statusMapping[v.value as ConsStatus] : v.value)}></Select>
</Form.Item>
<Form.Item name='comment' rules={[{ required: true, message: 'Комментарий необходим' }]} label='Комментарий'>
<Input.TextArea maxLength={1000} placeholder='Укажите комментарий к изменению статуса'></Input.TextArea>
</Form.Item>
</Form>
</Modal>
);
}