Нарушения проверок: expected/actual и индекс колонки nullable (#45) #46
@ -228,10 +228,13 @@ export interface UnresolvedLineOut {
|
||||
line_code_text: string | null;
|
||||
}
|
||||
export interface FailedCheckOut {
|
||||
column_index: number;
|
||||
/** -1 в GET …/statement и null в теле 409 — нарушение без колонки (P3-60) */
|
||||
column_index: number | null;
|
||||
relation: string;
|
||||
expected: string;
|
||||
actual: string;
|
||||
/** null — цель соотношения не заполнена при непустых членах (P3-30) */
|
||||
expected: string | null;
|
||||
/** допускает null по OpenAPI ядра (P3-30) */
|
||||
actual: string | null;
|
||||
codes: string[];
|
||||
line_ids: string[];
|
||||
}
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
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)}`;
|
||||
import { expectedActualText, failureKey, failureText } from './checkText';
|
||||
|
||||
type Props = {
|
||||
checks: ChecksOut;
|
||||
@ -20,7 +17,7 @@ type Props = {
|
||||
const failureList = (list: FailedCheckOut[], onShow?: (key: string) => void) => (
|
||||
<div style={{ maxHeight: 120, overflowY: 'auto' }}>
|
||||
{list.map((f) => (
|
||||
<div key={`${f.column_index}-${f.relation}`}>
|
||||
<div key={failureKey(f)}>
|
||||
{failureText(f)}
|
||||
{onShow && (f.line_ids ?? []).length > 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'
|
||||
showIcon
|
||||
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 && (
|
||||
|
||||
@ -8,6 +8,7 @@ import type { DocumentOut, EntryOut, FailedCheckOut } from '../../types/api';
|
||||
import { formatAmount } from '../../utils/money';
|
||||
import { IssuesPanel } from './IssuesPanel';
|
||||
import { confirmButtonStyle } from './ReviewCard';
|
||||
import { expectedActualText, failureKey } from './checkText';
|
||||
import { confirmErrorInfo } from './confirmError';
|
||||
import { AMOUNT_KEYS, type AmountKey, type OsvRow, flattenEntries } from './osvRows';
|
||||
import { osvBodyHeight } from './osvTableSize';
|
||||
@ -183,8 +184,8 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
|
||||
problem.failures.length > 0 ? (
|
||||
<ul style={{ margin: 0, paddingLeft: 18 }}>
|
||||
{problem.failures.map((f) => (
|
||||
<li key={`${f.column_index}-${f.relation}`}>
|
||||
{f.relation}: ожидалось {formatAmount(f.expected)}, получено {formatAmount(f.actual)}
|
||||
<li key={failureKey(f)}>
|
||||
{f.relation}: {expectedActualText(f)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -6,12 +6,13 @@ import { ApiError } from '../../api/client';
|
||||
import { acceptChecks, confirmDocument, getStatement, patchStatement } from '../../services/api/documents';
|
||||
import type { DocumentOut, FailedCheckOut, StatementOut, UnresolvedLineOut } from '../../types/api';
|
||||
import { parseAmountInput } from '../../utils/money';
|
||||
import { ChecksPanel, failureText } from './ChecksPanel';
|
||||
import { ChecksPanel } from './ChecksPanel';
|
||||
import { IssuesPanel } from './IssuesPanel';
|
||||
import { ReviewCard, confirmButtonStyle } from './ReviewCard';
|
||||
import { SourcePreview } from './SourcePreview';
|
||||
import { StatementHeaderBlock } from './StatementHeaderBlock';
|
||||
import { StatementTable } from './StatementTable';
|
||||
import { failureKey, failureText } from './checkText';
|
||||
import { codeOptions as buildCodeOptions, lineErrorTarget } from './codeSuggestions';
|
||||
import { unresolvedHint, unresolvedLines } from './confirmBlockers';
|
||||
import { confirmErrorInfo } from './confirmError';
|
||||
@ -205,7 +206,7 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
|
||||
|
||||
let confirmDescription: ReactNode;
|
||||
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) {
|
||||
confirmDescription = (
|
||||
<Button type='link' style={{ paddingInline: 0 }} onClick={() => focusRow(confirmLines[0].line_id)}>
|
||||
|
||||
44
src/widgets/DocumentReview/checkText.test.ts
Normal file
44
src/widgets/DocumentReview/checkText.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
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: «цель не заполнена»', () => {
|
||||
const text = failureText({ ...base, expected: null });
|
||||
expect(text).toBe('Колонка 1: 1600 = 1700 — цель не заполнена, получено 9,00');
|
||||
expect(text).not.toContain('NaN');
|
||||
});
|
||||
it('expected и actual оба null', () => {
|
||||
expect(failureText({ ...base, expected: null, actual: null })).toContain('цель не заполнена, получено —');
|
||||
});
|
||||
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}`;
|
||||
|
||||
/** «ожидалось …, получено …» для любых expected/actual, в том числе null. */
|
||||
export const expectedActualText = (f: Pick<FailedCheckOut, 'expected' | 'actual'>): string =>
|
||||
f.expected === null
|
||||
? `${NO_TARGET}, получено ${formatAmount(f.actual)}`
|
||||
: `ожидалось ${formatAmount(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