109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { Alert, Button, Input, Modal, Space, Tooltip, Typography } from 'antd';
|
||
import { useState } from 'react';
|
||
import type { ChecksOut, FailedCheckOut } from '../../types/api';
|
||
import { expectedActualText, failureKey, failureText } from './checkText';
|
||
import CustomModalCloseIcon from '../../components/CustomModalCloseIcon';
|
||
|
||
type Props = {
|
||
checks: ChecksOut;
|
||
editable: boolean;
|
||
dirty: boolean;
|
||
onAccept: (comment: string) => Promise<void>;
|
||
onShow?: (key: string) => void;
|
||
};
|
||
|
||
// soft_failed — мягкие соотношения §7 (2400, 2500, «сумма подстрок = родитель») и жёсткие в пределах округления;
|
||
// подтверждение они не блокируют
|
||
// длинный список нарушений (OCR) не должен выталкивать таблицу формы за экран
|
||
const failureList = (list: FailedCheckOut[], onShow?: (key: string) => void) => (
|
||
<div style={{ maxHeight: 120, overflowY: 'auto' }}>
|
||
{list.map((f) => (
|
||
<div key={failureKey(f)}>
|
||
{failureText(f)}
|
||
{onShow && (f.line_ids ?? []).length > 0 && (
|
||
<Button type='link' size='small' onClick={() => onShow(f.line_ids[0])}>
|
||
Показать
|
||
</Button>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
|
||
export function ChecksPanel({ checks, editable, dirty, onAccept, onShow }: Props) {
|
||
const [open, setOpen] = useState(false);
|
||
const [comment, setComment] = useState('');
|
||
const [saving, setSaving] = useState(false);
|
||
const canAccept = editable && checks.failed.length > 0 && checks.accepted === null;
|
||
|
||
const submit = async () => {
|
||
setSaving(true);
|
||
try {
|
||
await onAccept(comment.trim());
|
||
setOpen(false);
|
||
setComment('');
|
||
} catch {
|
||
// ошибку уже показал вызывающий; окно остаётся открытым, чтобы не потерять комментарий
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Space orientation='vertical' size={8} style={{ width: '100%' }}>
|
||
{checks.failed.length === 0 && checks.soft_failed.length === 0 && !checks.cross_form && (
|
||
<Alert type='success' showIcon title='Проверки пройдены' />
|
||
)}
|
||
{checks.failed.length > 0 && (
|
||
<Alert type='error' showIcon title={`Нарушения (${checks.failed.length})`} description={failureList(checks.failed, onShow)} />
|
||
)}
|
||
{checks.soft_failed.length > 0 && (
|
||
<Alert
|
||
type='warning'
|
||
showIcon
|
||
title={`Предупреждения проверок (${checks.soft_failed.length})`}
|
||
description={failureList(checks.soft_failed, onShow)}
|
||
/>
|
||
)}
|
||
{checks.cross_form && (
|
||
<Alert
|
||
type='warning'
|
||
showIcon
|
||
title='Межформенная проверка ББ↔ОФР'
|
||
description={`${checks.cross_form.relation}: ${expectedActualText(checks.cross_form)}`}
|
||
/>
|
||
)}
|
||
{checks.accepted && (
|
||
<Alert
|
||
type='info'
|
||
showIcon
|
||
title='Нарушения приняты'
|
||
description={`${new Date(checks.accepted.at).toLocaleString('ru-RU')}: ${checks.accepted.comment}`}
|
||
/>
|
||
)}
|
||
{canAccept && (
|
||
<Tooltip title={dirty ? 'Сначала сохраните изменения' : undefined}>
|
||
<Button disabled={dirty} onClick={() => setOpen(true)}>
|
||
Принять с комментарием
|
||
</Button>
|
||
</Tooltip>
|
||
)}
|
||
<Modal
|
||
open={open}
|
||
title='Принять нарушения'
|
||
okText='Принять'
|
||
cancelText='Отмена'
|
||
confirmLoading={saving}
|
||
centered
|
||
closeIcon={<CustomModalCloseIcon />}
|
||
|
||
okButtonProps={{ disabled: comment.trim().length === 0 }}
|
||
onOk={submit}
|
||
onCancel={() => setOpen(false)}>
|
||
<Typography.Paragraph>Комментарий обязателен (до 2000 символов).</Typography.Paragraph>
|
||
<Input.TextArea rows={4} maxLength={2000} value={comment} onChange={(e) => setComment(e.target.value)} />
|
||
</Modal>
|
||
</Space>
|
||
);
|
||
}
|