В soft_failed попадают мягкие соотношения §7 и жёсткие в пределах округления, а не только округление. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
import { Alert, Button, Input, Modal, Space, Tooltip, Typography } from 'antd';
|
||
import { useState } from 'react';
|
||
import type { ChecksOut, FailedCheckOut } from '../../types/api';
|
||
import { formatAmount } from '../../utils/money';
|
||
|
||
export const failureText = (f: FailedCheckOut) =>
|
||
`Колонка ${f.column_index + 1}: ${f.relation} — ожидалось ${formatAmount(f.expected)}, получено ${formatAmount(f.actual)}`;
|
||
|
||
type Props = { checks: ChecksOut; editable: boolean; dirty: boolean; onAccept: (comment: string) => Promise<void> };
|
||
|
||
// soft_failed — мягкие соотношения §7 (2400, 2500, «сумма подстрок = родитель») и жёсткие в пределах округления;
|
||
// подтверждение они не блокируют
|
||
// длинный список нарушений (OCR) не должен выталкивать таблицу формы за экран
|
||
const failureList = (list: FailedCheckOut[]) => (
|
||
<div style={{ maxHeight: 120, overflowY: 'auto' }}>
|
||
{list.map((f) => (
|
||
<div key={`${f.column_index}-${f.relation}`}>{failureText(f)}</div>
|
||
))}
|
||
</div>
|
||
);
|
||
|
||
export function ChecksPanel({ checks, editable, dirty, onAccept }: 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)} />
|
||
)}
|
||
{checks.soft_failed.length > 0 && (
|
||
<Alert type='warning' showIcon title={`Предупреждения проверок (${checks.soft_failed.length})`} description={failureList(checks.soft_failed)} />
|
||
)}
|
||
{checks.cross_form && (
|
||
<Alert
|
||
type='warning'
|
||
showIcon
|
||
title='Межформенная проверка ББ↔ОФР'
|
||
description={`${checks.cross_form.relation}: ожидалось ${formatAmount(checks.cross_form.expected)}, получено ${formatAmount(checks.cross_form.actual)}`}
|
||
/>
|
||
)}
|
||
{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}
|
||
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>
|
||
);
|
||
}
|