fix(web): «Изменить статус» — путь /consolidations/…/status, модалка по спеке, [31-00] (AURORA-1589, PR #166)
404 был из-за опечатки в пути (/consolidation/ без s). Модалка берёт статус из карточки страницы, а после 200 страница берёт карточку из ответа; в «Новом этапе» ничего не выбрано; ошибки — текст сервера, модалка остаётся открытой. Кнопка в шапке активна при своей блокировке и наличии ручных переходов, подсказка «Возьмите консолидацию в работу». Пояснение под заголовком — текст статуса, а не название. Модалка «Остались неуказанные ИНН» при уходе с «ОСВ» (вкладки шагов пока неактивны). Предупреждение браузера при начатом вводе. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
54de39727b
commit
ccca6dfa13
@ -1,8 +1,9 @@
|
||||
import { DownloadOutlined, FilterOutlined } from '@ant-design/icons';
|
||||
import { Alert, Button, InputNumber, Select, Space, Tooltip, message } from 'antd';
|
||||
import { Alert, Button, Select, Space, Tooltip, message } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ApiError } from '../api/client';
|
||||
import { useCurrentUser } from '../auth/MeContext';
|
||||
import PageTitle from '../components/PageTitle';
|
||||
import { TableTag } from '../components/TableTag/TableTag';
|
||||
import { TAG_PRESETS } from '../components/TableTag/presets';
|
||||
@ -11,18 +12,18 @@ import { exportOsv } from '../services/api/osv';
|
||||
import type { ConsolidatedRowOut } from '../types/api';
|
||||
import type { ConsRussianStatus } from '../types/consolidations/common_types';
|
||||
import type { VersionCard } from '../types/consolidations/version_members';
|
||||
import { CONS_STATUS_LABEL, canChangeStatus, consStatusDescription, isLockedBy } from '../utils/consolidations';
|
||||
import { exportFilename, saveBlob } from '../utils/download';
|
||||
import { quarterShortLabel } from '../utils/labels';
|
||||
import { consStatusDescriptionMapping, statusColorMapping, statusMapping } from '../widgets/ConsoleTable/consts';
|
||||
import { statusColorMapping } from '../widgets/ConsoleTable/consts';
|
||||
import { CounterpartyMapModal } from '../widgets/ConsolidatedOsv/CounterpartyMapModal';
|
||||
import { OsvGaps } from '../widgets/ConsolidatedOsv/OsvGaps';
|
||||
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 { PRECISIONS, UNITS, useNumberDisplay } from '../widgets/ConsolidatedOsv/useNumberDisplay';
|
||||
import ChangeStatusModal from '../widgets/StatusChangeModal/ChangeStatusModal';
|
||||
import { UNITS, useNumberDisplay } from '../widgets/ConsolidatedOsv/useNumberDisplay';
|
||||
|
||||
function OsvContent({ versionId }: { versionId: string }) {
|
||||
const [card, setCard] = useState<VersionCard | null>(null);
|
||||
@ -63,9 +64,12 @@ function OsvContent({ versionId }: { versionId: string }) {
|
||||
const s = osv.summary;
|
||||
const unmapped = s?.unmapped_total ?? 0;
|
||||
const onlyUnmapped = osv.filters.unmapped === true;
|
||||
const russianStatus = card ? (statusMapping[card.status] ?? card.status) : '';
|
||||
const description = card ? (consStatusDescriptionMapping[card.status] ?? russianStatus) : '';
|
||||
const russianStatus = card ? (CONS_STATUS_LABEL[card.status] ?? card.status) : '';
|
||||
// под заголовком — пояснение статуса, не его название
|
||||
const description = card ? consStatusDescription(card.status) : '';
|
||||
const preset = statusColorMapping[russianStatus as ConsRussianStatus] ?? TAG_PRESETS.default;
|
||||
const statusChangeAllowed = card ? canChangeStatus(card.status, card.lock, currentUser.id) : false;
|
||||
const ownLock = card ? isLockedBy(card.lock, currentUser.id) : false;
|
||||
const groupName = card?.company_group.name ?? '';
|
||||
const period = card ? `${card.year} · ${quarterShortLabel(card.quarter)}` : '';
|
||||
return (
|
||||
@ -91,28 +95,22 @@ function OsvContent({ versionId }: { versionId: string }) {
|
||||
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>
|
||||
) : (
|
||||
{/* активна при своей блокировке и наличии ручных переходов из текущего статуса */}
|
||||
<Tooltip title={ownLock ? undefined : 'Возьмите консолидацию в работу'}>
|
||||
<Button
|
||||
size='middle'
|
||||
onClick={() => {
|
||||
setModalOpen(true);
|
||||
}}
|
||||
style={{ fontSize: 12 }}
|
||||
type='default'>
|
||||
type='default'
|
||||
disabled={!statusChangeAllowed}
|
||||
onClick={() => setModalOpen(true)}>
|
||||
Изменить статус
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<OsvTabs />
|
||||
<OsvTabs unmappedTotal={unmapped} />
|
||||
<main
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
@ -134,7 +132,7 @@ function OsvContent({ versionId }: { versionId: string }) {
|
||||
<div style={{ fontSize: 10, color: '#667085' }}>Знаков после запятой</div>
|
||||
<Select
|
||||
variant='underlined'
|
||||
options={[0, 1, 2, 3].map((i) => ({ label: i, value: i }))}
|
||||
options={PRECISIONS.map((i) => ({ label: i, value: i }))}
|
||||
value={precision}
|
||||
onChange={(v) => setPrecision(v ?? 2)}
|
||||
style={{ width: 90 }}
|
||||
@ -196,14 +194,7 @@ function OsvContent({ versionId }: { versionId: string }) {
|
||||
onMapped={osv.refresh}
|
||||
/>
|
||||
)}
|
||||
<ChangeStatusModal
|
||||
open={statusModalOpen}
|
||||
versionId={versionId}
|
||||
onSuccess={setCard}
|
||||
setClose={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
{card && <ChangeStatusModal open={statusModalOpen} card={card} onSuccess={setCard} onClose={() => setModalOpen(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,20 +4,17 @@ import type { LockOut } from '../../types/consolidations/common_types';
|
||||
import type { IConsolidation } from '../../types/consolidations/consolidations';
|
||||
import type { ChecklistBackendShape, VersionCard } from '../../types/consolidations/version_members';
|
||||
|
||||
/** Список консолидаций ГК; `in_work_only` — серверный фильтр «Только в работе» (по умолчанию false). */
|
||||
export const getConsolidations = (groupId: string, in_work_only: boolean) =>
|
||||
api.get<IConsolidation[]>(`/company-groups/${groupId}/consolidations?in_work_only=${in_work_only}`);
|
||||
api.get<IConsolidation[]>(`/company-groups/${groupId}/consolidations`, { in_work_only });
|
||||
export const getVersionCard = (versionId: string) => api.get<VersionCard>(`/consolidations/versions/${versionId}`);
|
||||
export const createConsolidation = (body: ConsolidationCreate) => api.post<VersionCard>('/consolidations', body);
|
||||
export const downloadChecklistCsv = (versionId: string) => apiFetchBlob(`/consolidations/versions/${versionId}/checklist?format=csv`);
|
||||
export const getChecklist = (versionId: string) => api.get<ChecklistBackendShape>(`/consolidations/versions/${versionId}/checklist`);
|
||||
/** «Взять в работу»: 201 — взята, 200 — уже ваша; чужая — 409 `LOCKED_BY_OTHER` (перехват — с `takeover_reason`). */
|
||||
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);
|
||||
};
|
||||
export const lockVersion = (versionId: string, reason?: string | null) =>
|
||||
api.post<LockOut>(`/consolidations/versions/${versionId}/lock`, reason ? { takeover_reason: reason } : {});
|
||||
|
||||
/** Ручная смена статуса (гайд §5.7): нужна своя блокировка; ответ — карточка версии с новым статусом. */
|
||||
export const updateStatus = (versionId: string, body: VersionStatusUpdate) =>
|
||||
api.post<VersionCard>(`/consolidation/versions/${versionId}/status`, body);
|
||||
api.post<VersionCard>(`/consolidations/versions/${versionId}/status`, body);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// DTO API arm-core (/api/v1). Повторяют arm-core/api/src/schemas/; Decimal и даты — строки.
|
||||
|
||||
import { ConsStatus } from './consolidations/common_types';
|
||||
import type { ConsStatus } from './consolidations/common_types';
|
||||
|
||||
export type Quarter = 'Q1' | 'Q2' | 'Q3' | 'Q4';
|
||||
export type DocumentType = 'BALANCE' | 'INCOME' | 'OSV' | 'DISCLOSURE' | 'OTHER';
|
||||
|
||||
@ -1,26 +1,106 @@
|
||||
import { Segmented, Tooltip } from 'antd';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal, Segmented, Tooltip } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { needsUnmappedWarning } from '../../utils/consolidations';
|
||||
|
||||
const SOON = 'Будет в следующих срезах';
|
||||
const OSV = 'ОСВ';
|
||||
const TABS = ['Шахматка', 'Таблица корректировки', 'Гудвилл', 'КФО', 'КФО Общий', 'Подтверждение КФО клиента'];
|
||||
|
||||
/** Ряд вкладок шагов консолидации (макет [30]): активна только «ОСВ». */
|
||||
export function OsvTabs() {
|
||||
type Props = {
|
||||
/** `unmapped_total` сводки: больше нуля — при уходе с «ОСВ» показывается модалка [31-00]. */
|
||||
unmappedTotal: number;
|
||||
/** Переход на вкладку шага. Пока шагов нет — вкладки неактивны, переход не задан (шахматка — PR #158). */
|
||||
onNavigate?: (tab: string) => void;
|
||||
/** Вкладки, по которым уже можно перейти (остальные — «Будет в следующих срезах»). */
|
||||
enabledTabs?: readonly string[];
|
||||
};
|
||||
|
||||
/** Ряд вкладок шагов консолидации (макет [30]): «ОСВ» — текущая; уход с неё при несопоставленных строках — через [31-00]. */
|
||||
export function OsvTabs({ unmappedTotal, onNavigate, enabledTabs = [] }: Props) {
|
||||
const [pending, setPending] = useState<string | null>(null);
|
||||
|
||||
const select = (tab: string) => {
|
||||
if (tab === OSV || !onNavigate) return;
|
||||
if (needsUnmappedWarning(unmappedTotal)) {
|
||||
setPending(tab);
|
||||
} else {
|
||||
onNavigate(tab);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Segmented
|
||||
value='ОСВ'
|
||||
style={{ alignSelf: 'flex-start', maxWidth: '100%', overflowX: 'auto', overflowY: 'hidden' }}
|
||||
options={[
|
||||
{ value: 'ОСВ', label: <span style={{ color: '#2a8c3a', fontWeight: 600 }}>ОСВ</span> },
|
||||
...TABS.map((t) => ({
|
||||
value: t,
|
||||
disabled: true,
|
||||
label: (
|
||||
<Tooltip title={SOON}>
|
||||
<span>{t}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<>
|
||||
<Segmented
|
||||
value={OSV}
|
||||
onChange={(v) => select(String(v))}
|
||||
style={{ alignSelf: 'flex-start', maxWidth: '100%', overflowX: 'auto', overflowY: 'hidden' }}
|
||||
options={[
|
||||
{ value: OSV, label: <span style={{ color: '#2a8c3a', fontWeight: 600 }}>{OSV}</span> },
|
||||
...TABS.map((t) => {
|
||||
const enabled = enabledTabs.includes(t) && !!onNavigate;
|
||||
return {
|
||||
value: t,
|
||||
disabled: !enabled,
|
||||
label: enabled ? (
|
||||
t
|
||||
) : (
|
||||
<Tooltip title={SOON}>
|
||||
<span>{t}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
};
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
<UnmappedInnModal
|
||||
open={pending !== null}
|
||||
unmappedTotal={unmappedTotal}
|
||||
onStay={() => setPending(null)}
|
||||
onGo={() => {
|
||||
const tab = pending;
|
||||
setPending(null);
|
||||
if (tab && onNavigate) onNavigate(tab);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** [31-00] «Остались неуказанные ИНН»: «Остаться» — без перехода, «Перейти» — переход. */
|
||||
export function UnmappedInnModal({
|
||||
open,
|
||||
unmappedTotal,
|
||||
onStay,
|
||||
onGo,
|
||||
}: {
|
||||
open: boolean;
|
||||
unmappedTotal: number;
|
||||
onStay: () => void;
|
||||
onGo: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title='Остались неуказанные ИНН'
|
||||
centered
|
||||
onCancel={onStay}
|
||||
footer={[
|
||||
<Button key='stay' onClick={onStay}>
|
||||
Остаться
|
||||
</Button>,
|
||||
<Button key='go' type='primary' onClick={onGo}>
|
||||
Перейти
|
||||
</Button>,
|
||||
]}>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||
{/* треугольник с «!», как в макете [31-00] */}
|
||||
<WarningOutlined style={{ color: '#dc6803', fontSize: 20, marginTop: 2 }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>В ОСВ остались неуказанные ИНН</div>
|
||||
<div style={{ color: '#667085' }}>Не заполнено ИНН: {unmappedTotal}. Точно хотите перейти в другой раздел?</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,77 +1,55 @@
|
||||
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 { Form, Input, Modal, Select, message } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { ApiError } from '../../api/client';
|
||||
import { VersionCard } from '../../types/consolidations/version_members';
|
||||
import { TableTag } from '../../components/TableTag/TableTag';
|
||||
import { useBeforeUnload } from '../../hooks/useBeforeUnload';
|
||||
import { updateStatus } from '../../services/api/consolidations';
|
||||
import type { VersionStatusUpdate } from '../../types/api';
|
||||
import type { ConsStatus } from '../../types/consolidations/common_types';
|
||||
import type { VersionCard } from '../../types/consolidations/version_members';
|
||||
import { CONS_STATUS_LABEL, MAX_REASON_LENGTH, canSubmitStatusChange, manualTransitions } from '../../utils/consolidations';
|
||||
import { statusColorMapping } from '../ConsoleTable/consts';
|
||||
|
||||
type StatusModalProps = {
|
||||
open: boolean;
|
||||
versionId: string;
|
||||
setClose: () => void;
|
||||
/** Карточка версии со страницы: из неё текущий статус и допустимые переходы. */
|
||||
card: VersionCard;
|
||||
onClose: () => void;
|
||||
/** 200: карточка из ответа `POST …/status` — экран берёт её вместо повторного GET. */
|
||||
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>();
|
||||
type FormValues = { to_status?: ConsStatus; comment?: string };
|
||||
|
||||
/** «Изменить статус» [33-01]: варианты — ручные переходы (`MANUAL_TRANSITIONS`), по умолчанию ничего не выбрано. */
|
||||
export default function ChangeStatusModal({ open, card, onClose, onSuccess }: StatusModalProps) {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
// до монтирования формы useWatch отдаёт undefined — правила это учитывают
|
||||
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 currentLabel = CONS_STATUS_LABEL[card.status];
|
||||
const options = manualTransitions(card.status).map((s) => ({ value: s, label: CONS_STATUS_LABEL[s] }));
|
||||
const canSubmit = canSubmitStatusChange(nextStatus, comment);
|
||||
|
||||
useBeforeUnload(open && (!!nextStatus || !!comment?.trim()));
|
||||
|
||||
const handleOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
if (!nextStatus || !canSubmit || submitting) return;
|
||||
const body: VersionStatusUpdate = { to_status: nextStatus, comment: (comment ?? '').trim() };
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const newCard = await updateStatus(versionId, { ...values, comment: values.comment.trim() });
|
||||
const newCard = await updateStatus(card.id, body);
|
||||
onSuccess(newCard);
|
||||
setClose();
|
||||
message.success(`Статус изменён: ${CONS_STATUS_LABEL[newCard.status]}`);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.status === 409 && e.code) {
|
||||
if (known409Codes.includes(e.code)) {
|
||||
message.error(errorCodeMap[e.code]);
|
||||
}
|
||||
} else {
|
||||
message.error('Непредвиденная ошибка');
|
||||
}
|
||||
// 409 VERSION_NOT_EDITABLE / LOCK_REQUIRED / LOCKED_BY_OTHER / TRANSITION_NOT_ALLOWED, 422 COMMENT_REQUIRED —
|
||||
// текст сервера; модалка открыта, статус на экране прежний
|
||||
message.error(e instanceof ApiError ? e.message : 'Не удалось изменить статус');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -81,28 +59,23 @@ export default function ChangeStatusModal({ open, versionId, setClose, onSuccess
|
||||
title='Изменить статус'
|
||||
okText='Изменить статус'
|
||||
cancelText='Отмена'
|
||||
okButtonProps={{ disabled: isSubmitDisabled || loading }}
|
||||
loading={loading}
|
||||
okButtonProps={{ disabled: !canSubmit }}
|
||||
confirmLoading={submitting}
|
||||
onOk={handleOk}
|
||||
onCancel={setClose}
|
||||
onCancel={() => !submitting && onClose()}
|
||||
destroyOnHidden>
|
||||
<div style={{ display: 'flex' }}>
|
||||
Текущий этап: <TableTag text={russianStatus} {...badgePreset} />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||
<span style={{ color: '#667085' }}>Текущий этап</span>
|
||||
<TableTag text={currentLabel} {...statusColorMapping[currentLabel]} />
|
||||
</div>
|
||||
|
||||
{nextStatusOptions && (
|
||||
<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>
|
||||
)}
|
||||
<Form form={form} layout='vertical' requiredMark={false} preserve={false}>
|
||||
<Form.Item name='to_status' label='Новый этап'>
|
||||
<Select placeholder='Выберите этап' options={options} />
|
||||
</Form.Item>
|
||||
<Form.Item name='comment' label='Комментарий'>
|
||||
<Input.TextArea rows={3} maxLength={MAX_REASON_LENGTH} placeholder='Укажите комментарий к изменению статуса' />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user