diff --git a/src/widgets/DocumentReview/IssuesPanel.tsx b/src/widgets/DocumentReview/IssuesPanel.tsx index 9576f10..e679a66 100644 --- a/src/widgets/DocumentReview/IssuesPanel.tsx +++ b/src/widgets/DocumentReview/IssuesPanel.tsx @@ -1,8 +1,11 @@ import { ExclamationCircleOutlined, InfoCircleOutlined } from '@ant-design/icons'; import { Alert, Button, Tag } from 'antd'; +import { useState } from 'react'; import type { Severity } from '../../types/api'; 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 — синий. */ 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) { const plain = mode === 'document'; + const [expanded, setExpanded] = useState(false); if (items.length === 0) return null; + const { shown, hidden } = collapseIssues(items, expanded, COLLAPSED_LIMIT); return ( i.severity === 'error') ? 'error' : 'info'} @@ -29,7 +34,7 @@ export function IssuesPanel({ items, mode, onShow }: Props) { title={`Предупреждения (${items.length})`} description={
- {items.map((item) => ( + {shown.map((item) => (
{severityLabel(item.severity)} {item.message} @@ -41,6 +46,11 @@ export function IssuesPanel({ items, mode, onShow }: Props) { )}
))} + {(hidden > 0 || expanded) && ( + + )}
} /> diff --git a/src/widgets/DocumentReview/cellView.test.ts b/src/widgets/DocumentReview/cellView.test.ts index 0af48af..c636ee7 100644 --- a/src/widgets/DocumentReview/cellView.test.ts +++ b/src/widgets/DocumentReview/cellView.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; 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'; const val = (extra: Partial = {}): ValueOut => ({ @@ -138,3 +139,24 @@ describe('фон', () => { 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»'); + }); +}); diff --git a/src/widgets/DocumentReview/cellView.ts b/src/widgets/DocumentReview/cellView.ts index 747299d..74f6301 100644 --- a/src/widgets/DocumentReview/cellView.ts +++ b/src/widgets/DocumentReview/cellView.ts @@ -34,11 +34,22 @@ export function cellTipLines(value: ValueOut, issues: IssueOut[] = []): string[] if (value.text !== null && value.text.trim() !== '') lines.push(`Распознано: «${value.text}»`); const confidence = formatPercent(value.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('Подтверждено аналитиком'); for (const issue of issues) lines.push(issue.message); 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): текст, серый ли он, подсказка. */ export function codeCellView(row: GridRow): { text: string; muted: boolean; tip: string | undefined } { switch (row.lineStatus) { diff --git a/src/widgets/DocumentReview/issuePlaces.test.ts b/src/widgets/DocumentReview/issuePlaces.test.ts index 111cd9f..fde4e3e 100644 --- a/src/widgets/DocumentReview/issuePlaces.test.ts +++ b/src/widgets/DocumentReview/issuePlaces.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { IssueOut, StatementOut } from '../../types/api'; 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'; const statement = statementV2Json as unknown as StatementOut; @@ -82,3 +82,14 @@ describe('worstSeverity', () => { 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 }); + }); +}); diff --git a/src/widgets/DocumentReview/issuePlaces.ts b/src/widgets/DocumentReview/issuePlaces.ts index 550fbd2..7d82746 100644 --- a/src/widgets/DocumentReview/issuePlaces.ts +++ b/src/widgets/DocumentReview/issuePlaces.ts @@ -118,3 +118,9 @@ export function worstSeverity(issues: IssueOut[]): Severity | null { if (issues.length === 0) return null; return issues.some((i) => i.severity === 'error') ? 'error' : 'info'; } + +/** Свёрнутый список предупреждений (T11.11): первые `limit` (порядок уже «ошибки первыми») и сколько скрыто. */ +export function collapseIssues(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 }; +} diff --git a/src/widgets/DocumentReview/statementGrid.test.ts b/src/widgets/DocumentReview/statementGrid.test.ts index 849ee06..c44f59c 100644 --- a/src/widgets/DocumentReview/statementGrid.test.ts +++ b/src/widgets/DocumentReview/statementGrid.test.ts @@ -173,6 +173,10 @@ describe('parent_code и пустые строки детализации', () = ]; 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('подстрока с правкой в черновике не считается пустой', () => { const rows = [gridRow('11101', '1110', { c1: cell({}) }), gridRow('11102', '1110', { c1: cell({}) })]; expect([...emptySublineKeys(rows, { '11101:c1': '7' })]).toEqual(['11102']); diff --git a/src/widgets/DocumentReview/statementGrid.ts b/src/widgets/DocumentReview/statementGrid.ts index 3fd510a..a80c53d 100644 --- a/src/widgets/DocumentReview/statementGrid.ts +++ b/src/widgets/DocumentReview/statementGrid.ts @@ -88,6 +88,7 @@ export function emptySublineKeys( .filter( (row) => row.parentCode !== null && + row.lineStatus !== 'detail' && !edited.has(row.key) && !(row.lineId !== null && row.lineId in lineActions) && !Object.values(row.cells).some(hasValue),