diff --git a/web/src/layouts/ConsolidatedOsvPage.tsx b/web/src/layouts/ConsolidatedOsvPage.tsx index f9dcd0e..f76f74d 100644 --- a/web/src/layouts/ConsolidatedOsvPage.tsx +++ b/web/src/layouts/ConsolidatedOsvPage.tsx @@ -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(null); + const [statusModalOpen, setModalOpen] = useState(false); const [exporting, setExporting] = useState(false); // строка, для которой открыт выбор организации («Указать» или ИНН сопоставленного ЮЛ) const [mapping, setMapping] = useState(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 && ( - + Статус отчёта - + + {card.lock === null || (card.lock && card.lock.locked_by !== currentUser.id) ? ( + + + + ) : ( + + )} ) } @@ -144,6 +176,14 @@ function OsvContent({ versionId }: { versionId: string }) { onMapped={osv.refresh} /> )} + { + setModalOpen(false); + }} + /> ); } diff --git a/web/src/services/api/consolidations.ts b/web/src/services/api/consolidations.ts index af48a51..577f318 100644 --- a/web/src/services/api/consolidations.ts +++ b/web/src/services/api/consolidations.ts @@ -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(`/consolidations/versions/${versionId}/lock`, body); }; + +export const updateStatus = (versionId: string, body: VersionStatusUpdate) => + api.post(`/consolidation/versions/${versionId}/status`, body); diff --git a/web/src/types/api.ts b/web/src/types/api.ts index c6e0850..7b410ee 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -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; +} diff --git a/web/src/types/consolidations/common_types.ts b/web/src/types/consolidations/common_types.ts index cb9c08d..7d7b443 100644 --- a/web/src/types/consolidations/common_types.ts +++ b/web/src/types/consolidations/common_types.ts @@ -18,7 +18,7 @@ export type ConsRussianStatus = | 'Верификация РФ' | 'Инициация' | 'Обработка документов' - | 'Коррекция данных' + | 'Корректировка данных' | 'Консолидация ГО' | 'Консолидация РФ' | 'Отменён' diff --git a/web/src/widgets/ConsoleTable/consts.ts b/web/src/widgets/ConsoleTable/consts.ts index 590ae1e..1cb0a59 100644 --- a/web/src/widgets/ConsoleTable/consts.ts +++ b/web/src/widgets/ConsoleTable/consts.ts @@ -7,7 +7,7 @@ export const statusMapping: Record = { VERIFICATION_RF: 'Верификация РФ', INITIATED: 'Инициация', DOCUMENTS_PROCESSING: 'Обработка документов', - DATA_CORRECTION: 'Коррекция данных', + DATA_CORRECTION: 'Корректировка данных', CONSOLIDATION_GO: 'Консолидация ГО', CONSOLIDATION_RF: 'Консолидация РФ', CANCELLED: 'Отменён', @@ -25,7 +25,7 @@ export const statusColorMapping: Record = { 'Консолидация ГО': TAG_PRESETS.purple, 'Консолидация РФ': TAG_PRESETS.purple, Корректировка: TAG_PRESETS.gold, - 'Коррекция данных': TAG_PRESETS.gold, + 'Корректировка данных': TAG_PRESETS.gold, 'Обработка документов': TAG_PRESETS.gold, Отклонён: TAG_PRESETS.red, Отменён: TAG_PRESETS.red, diff --git a/web/src/widgets/StatusChangeModal/ChangeStatusModal.tsx b/web/src/widgets/StatusChangeModal/ChangeStatusModal.tsx new file mode 100644 index 0000000..9ee315c --- /dev/null +++ b/web/src/widgets/StatusChangeModal/ChangeStatusModal.tsx @@ -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 = { + 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(null); + const [nextStatusOptions, setNextStatusOptions] = useState([]); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + + 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 ( + +
+ Текущий этап: +
+ +
+ + + + + + +
+
+ ); +}