From 88a4920d7d9c45e5e2f698afec34c9578f36ef84 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 20:06:02 +0400 Subject: [PATCH] =?UTF-8?q?fix(arm-web):=20=D0=BD=D0=B5=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=B7=D0=BD=D0=B0=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=20=D1=81=D1=83=D0=BC=D0=BC=D1=8B,=20?= =?UTF-8?q?=D0=BF=D1=83=D1=81=D1=82=D1=8B=D0=B5=20=D0=BF=D0=BE=D0=B4=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D0=B1=D0=B5=D0=B7=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D1=88=D0=B8=D1=84=D1=80=D0=BE=D0=B2=D0=BE=D0=BA,?= =?UTF-8?q?=20=D1=81=D0=B2=D1=91=D1=80=D0=BD=D1=83=D1=82=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9=20?= =?UTF-8?q?(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T11.5 valueCellText, T11.10 emptySublineKeys пропускает detail, T11.11 collapseIssues (3 записи + «Показать все»). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W --- src/widgets/DocumentReview/IssuesPanel.tsx | 14 +++++++++-- src/widgets/DocumentReview/cellView.test.ts | 24 ++++++++++++++++++- src/widgets/DocumentReview/cellView.ts | 11 +++++++++ .../DocumentReview/issuePlaces.test.ts | 13 +++++++++- src/widgets/DocumentReview/issuePlaces.ts | 6 +++++ .../DocumentReview/statementGrid.test.ts | 4 ++++ src/widgets/DocumentReview/statementGrid.ts | 1 + 7 files changed, 69 insertions(+), 4 deletions(-) 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),