Нарушения проверок: expected/actual и индекс колонки могут быть null (#45)
Типы FailedCheckOut по API ядра (P3-30, P3-60); подпись «нет строки цели», «Без колонки» для -1/null; общий ключ списка. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d0095f325e
commit
cb36ec8840
@ -228,10 +228,12 @@ export interface UnresolvedLineOut {
|
|||||||
line_code_text: string | null;
|
line_code_text: string | null;
|
||||||
}
|
}
|
||||||
export interface FailedCheckOut {
|
export interface FailedCheckOut {
|
||||||
column_index: number;
|
/** -1 в GET …/statement и null в теле 409 — нарушение без колонки (P3-60) */
|
||||||
|
column_index: number | null;
|
||||||
relation: string;
|
relation: string;
|
||||||
expected: string;
|
/** null — у связи нет строки цели (P3-30) */
|
||||||
actual: string;
|
expected: string | null;
|
||||||
|
actual: string | null;
|
||||||
codes: string[];
|
codes: string[];
|
||||||
line_ids: string[];
|
line_ids: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import { Alert, Button, Input, Modal, Space, Tooltip, Typography } from 'antd';
|
import { Alert, Button, Input, Modal, Space, Tooltip, Typography } from 'antd';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { ChecksOut, FailedCheckOut } from '../../types/api';
|
import type { ChecksOut, FailedCheckOut } from '../../types/api';
|
||||||
import { formatAmount } from '../../utils/money';
|
import { expectedActualText, failureKey, failureText } from './checkText';
|
||||||
|
|
||||||
export const failureText = (f: FailedCheckOut) =>
|
|
||||||
`Колонка ${f.column_index + 1}: ${f.relation} — ожидалось ${formatAmount(f.expected)}, получено ${formatAmount(f.actual)}`;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
checks: ChecksOut;
|
checks: ChecksOut;
|
||||||
@ -20,7 +17,7 @@ type Props = {
|
|||||||
const failureList = (list: FailedCheckOut[], onShow?: (key: string) => void) => (
|
const failureList = (list: FailedCheckOut[], onShow?: (key: string) => void) => (
|
||||||
<div style={{ maxHeight: 120, overflowY: 'auto' }}>
|
<div style={{ maxHeight: 120, overflowY: 'auto' }}>
|
||||||
{list.map((f) => (
|
{list.map((f) => (
|
||||||
<div key={`${f.column_index}-${f.relation}`}>
|
<div key={failureKey(f)}>
|
||||||
{failureText(f)}
|
{failureText(f)}
|
||||||
{onShow && (f.line_ids ?? []).length > 0 && (
|
{onShow && (f.line_ids ?? []).length > 0 && (
|
||||||
<Button type='link' size='small' onClick={() => onShow(f.line_ids[0])}>
|
<Button type='link' size='small' onClick={() => onShow(f.line_ids[0])}>
|
||||||
@ -72,7 +69,7 @@ export function ChecksPanel({ checks, editable, dirty, onAccept, onShow }: Props
|
|||||||
type='warning'
|
type='warning'
|
||||||
showIcon
|
showIcon
|
||||||
title='Межформенная проверка ББ↔ОФР'
|
title='Межформенная проверка ББ↔ОФР'
|
||||||
description={`${checks.cross_form.relation}: ожидалось ${formatAmount(checks.cross_form.expected)}, получено ${formatAmount(checks.cross_form.actual)}`}
|
description={`${checks.cross_form.relation}: ${expectedActualText(checks.cross_form)}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{checks.accepted && (
|
{checks.accepted && (
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { DocumentOut, EntryOut, FailedCheckOut } from '../../types/api';
|
|||||||
import { formatAmount } from '../../utils/money';
|
import { formatAmount } from '../../utils/money';
|
||||||
import { IssuesPanel } from './IssuesPanel';
|
import { IssuesPanel } from './IssuesPanel';
|
||||||
import { confirmButtonStyle } from './ReviewCard';
|
import { confirmButtonStyle } from './ReviewCard';
|
||||||
|
import { expectedActualText, failureKey } from './checkText';
|
||||||
import { confirmErrorInfo } from './confirmError';
|
import { confirmErrorInfo } from './confirmError';
|
||||||
import { AMOUNT_KEYS, type AmountKey, type OsvRow, flattenEntries } from './osvRows';
|
import { AMOUNT_KEYS, type AmountKey, type OsvRow, flattenEntries } from './osvRows';
|
||||||
import { osvBodyHeight } from './osvTableSize';
|
import { osvBodyHeight } from './osvTableSize';
|
||||||
@ -183,8 +184,8 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
|
|||||||
problem.failures.length > 0 ? (
|
problem.failures.length > 0 ? (
|
||||||
<ul style={{ margin: 0, paddingLeft: 18 }}>
|
<ul style={{ margin: 0, paddingLeft: 18 }}>
|
||||||
{problem.failures.map((f) => (
|
{problem.failures.map((f) => (
|
||||||
<li key={`${f.column_index}-${f.relation}`}>
|
<li key={failureKey(f)}>
|
||||||
{f.relation}: ожидалось {formatAmount(f.expected)}, получено {formatAmount(f.actual)}
|
{f.relation}: {expectedActualText(f)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -6,12 +6,13 @@ import { ApiError } from '../../api/client';
|
|||||||
import { acceptChecks, confirmDocument, getStatement, patchStatement } from '../../services/api/documents';
|
import { acceptChecks, confirmDocument, getStatement, patchStatement } from '../../services/api/documents';
|
||||||
import type { DocumentOut, FailedCheckOut, StatementOut, UnresolvedLineOut } from '../../types/api';
|
import type { DocumentOut, FailedCheckOut, StatementOut, UnresolvedLineOut } from '../../types/api';
|
||||||
import { parseAmountInput } from '../../utils/money';
|
import { parseAmountInput } from '../../utils/money';
|
||||||
import { ChecksPanel, failureText } from './ChecksPanel';
|
import { ChecksPanel } from './ChecksPanel';
|
||||||
import { IssuesPanel } from './IssuesPanel';
|
import { IssuesPanel } from './IssuesPanel';
|
||||||
import { ReviewCard, confirmButtonStyle } from './ReviewCard';
|
import { ReviewCard, confirmButtonStyle } from './ReviewCard';
|
||||||
import { SourcePreview } from './SourcePreview';
|
import { SourcePreview } from './SourcePreview';
|
||||||
import { StatementHeaderBlock } from './StatementHeaderBlock';
|
import { StatementHeaderBlock } from './StatementHeaderBlock';
|
||||||
import { StatementTable } from './StatementTable';
|
import { StatementTable } from './StatementTable';
|
||||||
|
import { failureKey, failureText } from './checkText';
|
||||||
import { codeOptions as buildCodeOptions, lineErrorTarget } from './codeSuggestions';
|
import { codeOptions as buildCodeOptions, lineErrorTarget } from './codeSuggestions';
|
||||||
import { unresolvedHint, unresolvedLines } from './confirmBlockers';
|
import { unresolvedHint, unresolvedLines } from './confirmBlockers';
|
||||||
import { confirmErrorInfo } from './confirmError';
|
import { confirmErrorInfo } from './confirmError';
|
||||||
@ -205,7 +206,7 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
|
|||||||
|
|
||||||
let confirmDescription: ReactNode;
|
let confirmDescription: ReactNode;
|
||||||
if (confirmFailures.length > 0) {
|
if (confirmFailures.length > 0) {
|
||||||
confirmDescription = confirmFailures.map((f) => <div key={`${f.column_index}-${f.relation}`}>{failureText(f)}</div>);
|
confirmDescription = confirmFailures.map((f) => <div key={failureKey(f)}>{failureText(f)}</div>);
|
||||||
} else if (confirmLines.length > 0) {
|
} else if (confirmLines.length > 0) {
|
||||||
confirmDescription = (
|
confirmDescription = (
|
||||||
<Button type='link' style={{ paddingInline: 0 }} onClick={() => focusRow(confirmLines[0].line_id)}>
|
<Button type='link' style={{ paddingInline: 0 }} onClick={() => focusRow(confirmLines[0].line_id)}>
|
||||||
|
|||||||
41
src/widgets/DocumentReview/checkText.test.ts
Normal file
41
src/widgets/DocumentReview/checkText.test.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { FailedCheckOut } from '../../types/api';
|
||||||
|
import { failureKey, failureText, hasColumn } from './checkText';
|
||||||
|
|
||||||
|
const base: FailedCheckOut = { column_index: 0, relation: '1600 = 1700', expected: '10.00', actual: '9.00', codes: [], line_ids: [] };
|
||||||
|
|
||||||
|
describe('hasColumn', () => {
|
||||||
|
it('0 и больше — колонка есть; -1 и null — нарушение без колонки', () => {
|
||||||
|
expect(hasColumn(0)).toBe(true);
|
||||||
|
expect(hasColumn(3)).toBe(true);
|
||||||
|
expect(hasColumn(-1)).toBe(false);
|
||||||
|
expect(hasColumn(null)).toBe(false);
|
||||||
|
expect(hasColumn(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('failureText', () => {
|
||||||
|
it('обычное нарушение с номером колонки', () => {
|
||||||
|
expect(failureText(base)).toBe('Колонка 1: 1600 = 1700 — ожидалось 10,00, получено 9,00');
|
||||||
|
});
|
||||||
|
it('expected = null: «нет строки цели», без NaN', () => {
|
||||||
|
const text = failureText({ ...base, expected: null });
|
||||||
|
expect(text).toContain('ожидалось нет строки цели');
|
||||||
|
expect(text).not.toContain('NaN');
|
||||||
|
});
|
||||||
|
it('actual = null показывается прочерком', () => {
|
||||||
|
expect(failureText({ ...base, actual: null })).toContain('получено —');
|
||||||
|
});
|
||||||
|
it('индекс -1 и null: без номера колонки', () => {
|
||||||
|
expect(failureText({ ...base, column_index: -1 })).toMatch(/^Без колонки: /);
|
||||||
|
expect(failureText({ ...base, column_index: null })).toMatch(/^Без колонки: /);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('failureKey', () => {
|
||||||
|
it('для нарушений без колонки ключ не содержит -1/null', () => {
|
||||||
|
expect(failureKey({ ...base, column_index: null })).toBe('none-1600 = 1700');
|
||||||
|
expect(failureKey({ ...base, column_index: -1 })).toBe('none-1600 = 1700');
|
||||||
|
expect(failureKey(base)).toBe('0-1600 = 1700');
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/widgets/DocumentReview/checkText.ts
Normal file
25
src/widgets/DocumentReview/checkText.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { FailedCheckOut } from '../../types/api';
|
||||||
|
import { formatAmount } from '../../utils/money';
|
||||||
|
|
||||||
|
/** Подпись пустой цели: expected = null — у связи нет строки-цели (P3-30). */
|
||||||
|
const NO_TARGET = 'нет строки цели';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Есть ли у нарушения колонка. Ядро кодирует «колонки нет» двумя способами:
|
||||||
|
* -1 в ответе GET …/statement и null в теле 409 CHECKS_NOT_PASSED (P3-60).
|
||||||
|
*/
|
||||||
|
export const hasColumn = (index: number | null | undefined): index is number => typeof index === 'number' && index >= 0;
|
||||||
|
|
||||||
|
export const failureKey = (f: Pick<FailedCheckOut, 'column_index' | 'relation'>): string =>
|
||||||
|
`${hasColumn(f.column_index) ? f.column_index : 'none'}-${f.relation}`;
|
||||||
|
|
||||||
|
const expectedText = (v: string | null): string => (v === null ? NO_TARGET : formatAmount(v));
|
||||||
|
|
||||||
|
/** «ожидалось …, получено …» для любых expected/actual, в том числе null. */
|
||||||
|
export const expectedActualText = (f: Pick<FailedCheckOut, 'expected' | 'actual'>): string =>
|
||||||
|
`ожидалось ${expectedText(f.expected)}, получено ${formatAmount(f.actual)}`;
|
||||||
|
|
||||||
|
export const failureText = (f: FailedCheckOut): string => {
|
||||||
|
const where = hasColumn(f.column_index) ? `Колонка ${f.column_index + 1}` : 'Без колонки';
|
||||||
|
return `${where}: ${f.relation} — ${expectedActualText(f)}`;
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user