fix(arm-web): нераспознанный текст суммы, пустые подстроки без расшифровок, свёрнутый список предупреждений (#43)
T11.5 valueCellText, T11.10 emptySublineKeys пропускает detail, T11.11 collapseIssues (3 записи + «Показать все»). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
a3199d636e
commit
88a4920d7d
@ -1,8 +1,11 @@
|
|||||||
import { ExclamationCircleOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Alert, Button, Tag } from 'antd';
|
import { Alert, Button, Tag } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
import type { Severity } from '../../types/api';
|
import type { Severity } from '../../types/api';
|
||||||
import { severityLabel } from '../../utils/labels';
|
import { severityLabel } from '../../utils/labels';
|
||||||
import type { IssueListItem } from './issuePlaces';
|
import { type IssueListItem, collapseIssues } from './issuePlaces';
|
||||||
|
|
||||||
|
const COLLAPSED_LIMIT = 3; // сколько записей видно в свёрнутом списке (T11.11)
|
||||||
|
|
||||||
/** Значок уровня (P6-15): error — красный, info — синий. */
|
/** Значок уровня (P6-15): error — красный, info — синий. */
|
||||||
export function SeverityIcon({ severity, style }: { severity: Severity; style?: React.CSSProperties }) {
|
export function SeverityIcon({ severity, style }: { severity: Severity; style?: React.CSSProperties }) {
|
||||||
@ -21,7 +24,9 @@ type Props = { items: IssueListItem[]; mode: 'statement' | 'document'; onShow?:
|
|||||||
*/
|
*/
|
||||||
export function IssuesPanel({ items, mode, onShow }: Props) {
|
export function IssuesPanel({ items, mode, onShow }: Props) {
|
||||||
const plain = mode === 'document';
|
const plain = mode === 'document';
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) return null;
|
||||||
|
const { shown, hidden } = collapseIssues(items, expanded, COLLAPSED_LIMIT);
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
type={items.some((i) => i.severity === 'error') ? 'error' : 'info'}
|
type={items.some((i) => i.severity === 'error') ? 'error' : 'info'}
|
||||||
@ -29,7 +34,7 @@ export function IssuesPanel({ items, mode, onShow }: Props) {
|
|||||||
title={`Предупреждения (${items.length})`}
|
title={`Предупреждения (${items.length})`}
|
||||||
description={
|
description={
|
||||||
<div style={{ maxHeight: 120, overflowY: 'auto' }}>
|
<div style={{ maxHeight: 120, overflowY: 'auto' }}>
|
||||||
{items.map((item) => (
|
{shown.map((item) => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<Tag color={item.severity === 'error' ? 'error' : 'processing'}>{severityLabel(item.severity)}</Tag>
|
<Tag color={item.severity === 'error' ? 'error' : 'processing'}>{severityLabel(item.severity)}</Tag>
|
||||||
{item.message}
|
{item.message}
|
||||||
@ -41,6 +46,11 @@ export function IssuesPanel({ items, mode, onShow }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{(hidden > 0 || expanded) && (
|
||||||
|
<Button type='link' size='small' style={{ paddingInline: 0 }} onClick={() => setExpanded(!expanded)}>
|
||||||
|
{expanded ? 'Свернуть' : `Показать все (${items.length})`}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { ValueOut } from '../../types/api';
|
import type { ValueOut } from '../../types/api';
|
||||||
import { COLORS, cellTipLines, codeCellView, rowBackground, valueBackground } from './cellView';
|
import { formatAmount } from '../../utils/money';
|
||||||
|
import { COLORS, cellTipLines, codeCellView, rowBackground, valueBackground, valueCellText } from './cellView';
|
||||||
import type { GridRow } from './statementGrid';
|
import type { GridRow } from './statementGrid';
|
||||||
|
|
||||||
const val = (extra: Partial<ValueOut> = {}): ValueOut => ({
|
const val = (extra: Partial<ValueOut> = {}): ValueOut => ({
|
||||||
@ -138,3 +139,24 @@ describe('фон', () => {
|
|||||||
expect(rowBackground(row())).toBeUndefined();
|
expect(rowBackground(row())).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('valueCellText (T11.5)', () => {
|
||||||
|
it('сумма есть — обычный текст', () => {
|
||||||
|
expect(valueCellText(val({ amount: '1500.00' }))).toEqual({
|
||||||
|
text: formatAmount('1500.00', { fraction: 'auto', empty: '-' }),
|
||||||
|
muted: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('суммы нет, есть string_value — он серым', () => {
|
||||||
|
expect(valueCellText(val({ amount: null, string_value: '211(1 |' }))).toEqual({ text: '211(1 |', muted: true });
|
||||||
|
});
|
||||||
|
it('ничего нет — прочерк', () => {
|
||||||
|
expect(valueCellText(val({ amount: null }))).toEqual({ text: '-', muted: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cellTipLines и string_value (T11.5)', () => {
|
||||||
|
it('без суммы показывает нераспознанный string_value', () => {
|
||||||
|
expect(cellTipLines(val({ amount: null, string_value: 'abc' }))).toContain('Не распознано как сумма: «abc»');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -34,11 +34,22 @@ export function cellTipLines(value: ValueOut, issues: IssueOut[] = []): string[]
|
|||||||
if (value.text !== null && value.text.trim() !== '') lines.push(`Распознано: «${value.text}»`);
|
if (value.text !== null && value.text.trim() !== '') lines.push(`Распознано: «${value.text}»`);
|
||||||
const confidence = formatPercent(value.confidence);
|
const confidence = formatPercent(value.confidence);
|
||||||
if (confidence !== null) lines.push(`Уверенность: ${confidence}`);
|
if (confidence !== null) lines.push(`Уверенность: ${confidence}`);
|
||||||
|
if (value.amount === null && value.string_value !== null && value.string_value.trim() !== '') {
|
||||||
|
lines.push(`Не распознано как сумма: «${value.string_value}»`);
|
||||||
|
}
|
||||||
if (value.is_confirmed) lines.push('Подтверждено аналитиком');
|
if (value.is_confirmed) lines.push('Подтверждено аналитиком');
|
||||||
for (const issue of issues) lines.push(issue.message);
|
for (const issue of issues) lines.push(issue.message);
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Текст ячейки суммы (T11.5): сумма; без неё нераспознанный string_value серым; иначе прочерк. */
|
||||||
|
export function valueCellText(value: ValueOut): { text: string; muted: boolean } {
|
||||||
|
if (value.amount === null && value.string_value !== null && value.string_value.trim() !== '') {
|
||||||
|
return { text: value.string_value, muted: true };
|
||||||
|
}
|
||||||
|
return { text: formatAmount(value.amount, { fraction: 'auto', empty: '-' }), muted: false };
|
||||||
|
}
|
||||||
|
|
||||||
/** Ячейка кода (P6-16): текст, серый ли он, подсказка. */
|
/** Ячейка кода (P6-16): текст, серый ли он, подсказка. */
|
||||||
export function codeCellView(row: GridRow): { text: string; muted: boolean; tip: string | undefined } {
|
export function codeCellView(row: GridRow): { text: string; muted: boolean; tip: string | undefined } {
|
||||||
switch (row.lineStatus) {
|
switch (row.lineStatus) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { IssueOut, StatementOut } from '../../types/api';
|
import type { IssueOut, StatementOut } from '../../types/api';
|
||||||
import statementV2Json from './__fixtures__/statement-v2.json';
|
import statementV2Json from './__fixtures__/statement-v2.json';
|
||||||
import { issueListItems, issuePlaces, worstSeverity } from './issuePlaces';
|
import { collapseIssues, issueListItems, issuePlaces, worstSeverity } from './issuePlaces';
|
||||||
import { buildStatementGrid, cellKey } from './statementGrid';
|
import { buildStatementGrid, cellKey } from './statementGrid';
|
||||||
|
|
||||||
const statement = statementV2Json as unknown as StatementOut;
|
const statement = statementV2Json as unknown as StatementOut;
|
||||||
@ -82,3 +82,14 @@ describe('worstSeverity', () => {
|
|||||||
expect(worstSeverity([])).toBeNull();
|
expect(worstSeverity([])).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('collapseIssues (T11.11)', () => {
|
||||||
|
const items = Array.from({ length: 6 }, (_, i) => ({ id: String(i) })) as never[];
|
||||||
|
it('свёрнуто: первые 3, скрыто 3', () => {
|
||||||
|
expect(collapseIssues(items, false, 3)).toEqual({ shown: items.slice(0, 3), hidden: 3 });
|
||||||
|
});
|
||||||
|
it('развёрнуто или короткий список: всё', () => {
|
||||||
|
expect(collapseIssues(items, true, 3)).toEqual({ shown: items, hidden: 0 });
|
||||||
|
expect(collapseIssues(items.slice(0, 3), false, 3)).toEqual({ shown: items.slice(0, 3), hidden: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -118,3 +118,9 @@ export function worstSeverity(issues: IssueOut[]): Severity | null {
|
|||||||
if (issues.length === 0) return null;
|
if (issues.length === 0) return null;
|
||||||
return issues.some((i) => i.severity === 'error') ? 'error' : 'info';
|
return issues.some((i) => i.severity === 'error') ? 'error' : 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Свёрнутый список предупреждений (T11.11): первые `limit` (порядок уже «ошибки первыми») и сколько скрыто. */
|
||||||
|
export function collapseIssues<T>(items: T[], expanded: boolean, limit: number): { shown: T[]; hidden: number } {
|
||||||
|
if (expanded || items.length <= limit) return { shown: items, hidden: 0 };
|
||||||
|
return { shown: items.slice(0, limit), hidden: items.length - limit };
|
||||||
|
}
|
||||||
|
|||||||
@ -173,6 +173,10 @@ describe('parent_code и пустые строки детализации', () =
|
|||||||
];
|
];
|
||||||
expect([...emptySublineKeys(rows, {})]).toEqual(['11101', '11102']);
|
expect([...emptySublineKeys(rows, {})]).toEqual(['11101', '11102']);
|
||||||
});
|
});
|
||||||
|
it('расшифровка (lineStatus detail) с parent_code не считается пустой подстрокой (T11.10)', () => {
|
||||||
|
const detail = { ...gridRow('d1', '1110', { c1: cell({}) }), lineStatus: 'detail' as const };
|
||||||
|
expect([...emptySublineKeys([detail], {})]).toEqual([]);
|
||||||
|
});
|
||||||
it('подстрока с правкой в черновике не считается пустой', () => {
|
it('подстрока с правкой в черновике не считается пустой', () => {
|
||||||
const rows = [gridRow('11101', '1110', { c1: cell({}) }), gridRow('11102', '1110', { c1: cell({}) })];
|
const rows = [gridRow('11101', '1110', { c1: cell({}) }), gridRow('11102', '1110', { c1: cell({}) })];
|
||||||
expect([...emptySublineKeys(rows, { '11101:c1': '7' })]).toEqual(['11102']);
|
expect([...emptySublineKeys(rows, { '11101:c1': '7' })]).toEqual(['11102']);
|
||||||
|
|||||||
@ -88,6 +88,7 @@ export function emptySublineKeys(
|
|||||||
.filter(
|
.filter(
|
||||||
(row) =>
|
(row) =>
|
||||||
row.parentCode !== null &&
|
row.parentCode !== null &&
|
||||||
|
row.lineStatus !== 'detail' &&
|
||||||
!edited.has(row.key) &&
|
!edited.has(row.key) &&
|
||||||
!(row.lineId !== null && row.lineId in lineActions) &&
|
!(row.lineId !== null && row.lineId in lineActions) &&
|
||||||
!Object.values(row.cells).some(hasValue),
|
!Object.values(row.cells).some(hasValue),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user