85 lines
4.0 KiB
TypeScript
85 lines
4.0 KiB
TypeScript
import { Form, Input, Modal, Select, message } from 'antd';
|
||
import { useState } from 'react';
|
||
import { ApiError } from '../../api/client';
|
||
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';
|
||
import CustomModalCloseIcon from '../../components/CustomModalCloseIcon';
|
||
|
||
type StatusModalProps = {
|
||
open: boolean;
|
||
/** Карточка версии со страницы: из неё текущий статус и допустимые переходы. */
|
||
card: VersionCard;
|
||
onClose: () => void;
|
||
/** 200: карточка из ответа `POST …/status` — экран берёт её вместо повторного GET. */
|
||
onSuccess: (card: VersionCard) => void;
|
||
};
|
||
|
||
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);
|
||
|
||
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 () => {
|
||
if (!nextStatus || !canSubmit || submitting) return;
|
||
const body: VersionStatusUpdate = { to_status: nextStatus, comment: (comment ?? '').trim() };
|
||
setSubmitting(true);
|
||
try {
|
||
const newCard = await updateStatus(card.id, body);
|
||
onSuccess(newCard);
|
||
message.success(`Статус изменён: ${CONS_STATUS_LABEL[newCard.status]}`);
|
||
onClose();
|
||
} catch (e) {
|
||
// 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);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
title='Изменить статус'
|
||
okText='Изменить статус'
|
||
cancelText='Отмена'
|
||
okButtonProps={{ disabled: !canSubmit }}
|
||
confirmLoading={submitting}
|
||
onOk={handleOk}
|
||
onCancel={() => !submitting && onClose()}
|
||
destroyOnHidden
|
||
centered
|
||
closeIcon={<CustomModalCloseIcon/>}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||
<span style={{ color: '#667085' }}>Текущий этап</span>
|
||
<TableTag text={currentLabel} {...statusColorMapping[currentLabel]} />
|
||
</div>
|
||
<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>
|
||
);
|
||
}
|