arm-core/web/src/widgets/DocumentReview/StatementTable.tsx
Andrey Epifancev 4ba339914c fix(web): синхронизация встроенного фронта с arm-web@test (1237105) (#120)
Перенесён диапазон arm-web 025233f..1237105 (3 коммита, 8 файлов набора синхронизации)
по шагам docs/scripts/sync-web.sh. После переноса web/ и arm-web@test идентичны по набору.
Коммиты arm-web:
- 54f88e9 feat(DocumentReview): подстроки под родителем, пустые строки детализации свёрнуты (#34, AURORA-1527)
- 85e1b86 fix(ChecksPanel): заголовок мягких проверок «Предупреждения проверок (N)» (#34, AURORA-1527)
- a4f5481 fix(DocumentReview): label переключателя пустых строк связан через htmlFor (#36, AURORA-1527)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:41:01 +04:00

120 lines
4.6 KiB
TypeScript

import { Input, Table, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { ColumnOut, DocumentType, ValueOut } from '../../types/api';
import { formatAmount, parseAmountInput } from '../../utils/money';
import { type LayoutRow, formLayout } from './formLayout';
import { columnTitle } from './reviewView';
import { type GridRow, cellFlags, cellKey } from './statementGrid';
type Props = {
documentType: DocumentType;
columns: ColumnOut[];
rows: GridRow[];
editable: boolean;
draft: Record<string, string>;
onDraftChange: (key: string, text: string, originalAmount: string | null) => void;
};
const ATTENTION_BG = '#fff7e6';
const MODIFIED_BG = '#e6f4ff';
function cellBackground(value: ValueOut | undefined): string | undefined {
const flags = cellFlags(value);
if (flags.modified) {
return MODIFIED_BG;
}
return flags.attention ? ATTENTION_BG : undefined;
}
function cellTip(value: ValueOut): string | undefined {
if (value.is_modified) {
return `Исходное: ${formatAmount(value.original_amount, { fraction: 'auto', empty: '-' })}`;
}
return value.confidence ? `Уверенность: ${value.confidence}%` : undefined;
}
const CHILD_INDENT = 20; // отступ названия строки детализации под родителем
const SECTION_BG = '#f2f4f7';
const GRAND_BG = '#f9f0ff'; // строки «БАЛАНС»: фиолетовый не занят (серый — разделы, синий — исправлено, оранжевый — внимание)
function lineWeight(r: LayoutRow): number | undefined {
if (r.kind !== 'line') return undefined;
if (r.grand) return 700;
return r.total ? 600 : undefined;
}
const lineBackground = (r: LayoutRow) => (r.kind === 'line' && r.grand ? GRAND_BG : undefined);
export function StatementTable({ documentType, columns, rows, editable, draft, onDraftChange }: Props) {
const layout = formLayout(documentType, rows);
const span = columns.length + 2;
// строка-заголовок раздела занимает всю ширину: первая ячейка colSpan = все колонки, остальные скрыты
const sectionSpan = (r: LayoutRow, first: boolean) => (r.kind === 'section' ? { colSpan: first ? span : 0 } : {});
const tableColumns: ColumnsType<LayoutRow> = [
{
key: 'name',
title: 'Наименование показателя',
onCell: (r) => ({
...sectionSpan(r, true),
style:
r.kind === 'section'
? { background: SECTION_BG, textAlign: 'center', fontWeight: r.level === 1 ? 700 : 600 }
: { background: lineBackground(r) },
}),
render: (_: unknown, r: LayoutRow) => {
if (r.kind === 'section') return r.title;
const text = (
<span style={{ fontWeight: lineWeight(r), paddingLeft: r.child ? CHILD_INDENT : undefined, display: 'inline-block' }}>
{r.displayName || '—'}
</span>
);
return r.fullName ? <Tooltip title={`Распознано: ${r.fullName}`}>{text}</Tooltip> : text;
},
},
{
key: 'code',
title: 'Код',
width: 60,
align: 'center' as const,
onCell: (r) => (r.kind === 'section' ? sectionSpan(r, false) : { style: { background: lineBackground(r) } }),
render: (_: unknown, r: LayoutRow) => (r.kind === 'line' ? <span style={{ fontWeight: lineWeight(r) }}>{r.row.code}</span> : null),
},
...columns.map((col) => ({
key: col.id,
title: columnTitle(col),
width: 112,
align: 'right' as const,
onCell: (r: LayoutRow) =>
r.kind === 'section'
? sectionSpan(r, false)
: { style: { background: cellBackground(r.row.cells[col.id]) ?? lineBackground(r), fontWeight: lineWeight(r) } },
render: (_: unknown, r: LayoutRow) => {
if (r.kind === 'section') return null;
const row = r.row;
const value = row.cells[col.id];
if (!value) {
return null;
}
const key = cellKey(row.key, col.id);
const tip = cellTip(value);
if (!editable) {
return <Tooltip title={tip}>{formatAmount(value.amount, { fraction: 'auto', empty: '-' })}</Tooltip>;
}
const text = draft[key] ?? value.amount ?? '';
const invalid = key in draft && !parseAmountInput(text).ok;
return (
<Tooltip title={invalid ? 'Неверный формат суммы' : tip}>
<Input
size='small'
status={invalid ? 'error' : undefined}
value={text}
onChange={(e) => onDraftChange(key, e.target.value, value.amount)}
style={{ textAlign: 'right' }}
/>
</Tooltip>
);
},
})),
];
return <Table<LayoutRow> size='small' bordered rowKey='key' columns={tableColumns} dataSource={layout} pagination={false} />;
}