arm-web/src/widgets/DocumentReview/ChecksPanel.tsx
Andrey Epifancev 85e1b86bb1 fix(ChecksPanel): заголовок мягких проверок «Предупреждения проверок (N)» (#34, AURORA-1527)
В soft_failed попадают мягкие соотношения §7 и жёсткие в пределах округления, а не только округление.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:09:37 +04:00

90 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}