fix(arm-web): вёрстка экрана проверки формы — ширины колонок, значки в ячейке, шапка, ссылка кнопкой (#43)

T11.1–T11.4, T11.6–T11.8: короткая подпись кнопки даты, фиксированная раскладка таблиц, код без переноса, значок уровня внутри ячейки, подсказка на всю ячейку, отступ child-строки, ссылка «Разберите строки без кода» — Button type=link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
Andrey Epifancev 2026-10-18 20:06:02 +04:00
parent 12fcea7560
commit a3199d636e
5 changed files with 62 additions and 32 deletions

View File

@ -51,9 +51,18 @@ export function ColumnHeader({ col, issues, editing, draftDate, error = false, o
style={{ marginTop: 4, fontWeight: 400 }}
/>
{canConfirm && (
<Button size='small' style={{ marginTop: 4 }} disabled={pending} onClick={() => onConfirmDate?.(col)}>
{pending ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}
</Button>
// короткая подпись, чтобы кнопка помещалась в колонку 112 px и ширина не прыгала (T11.1); длинный текст — в подсказке
<Tooltip title={pending ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}>
<Button
size='small'
block
style={{ marginTop: 4, paddingInline: 0, fontSize: 12 }}
disabled={pending}
aria-label={pending ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}
onClick={() => onConfirmDate?.(col)}>
{pending ? 'Подтверждено' : 'Подтвердить'}
</Button>
</Tooltip>
)}
</div>
);

View File

@ -45,7 +45,12 @@ export function StatementHeaderBlock({ header, documentType, editing, draft, onD
const reportDate = formatReportDate(header.report_date.value);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<table style={{ alignSelf: 'flex-end', borderCollapse: 'collapse', fontSize: 13 }}>
<table style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse', fontSize: 13 }}>
{/* фиксированная раскладка: ширина блока не зависит от содержимого и подписи кнопки (T11.6) */}
<colgroup>
<col style={{ width: 170 }} />
<col />
</colgroup>
<tbody>
{headerFields(header, byField).map((r) => (
<tr key={r.field}>
@ -54,7 +59,6 @@ export function StatementHeaderBlock({ header, documentType, editing, draft, onD
style={{
...cell,
fontWeight: 600,
minWidth: r.field === 'organization_name' ? 320 : 160,
background: fieldBackground(r.modified, r.attention),
}}>
{r.issues.length > 0 && (
@ -74,9 +78,16 @@ export function StatementHeaderBlock({ header, documentType, editing, draft, onD
onChange={(e) => onDraftChange(r.field, e.target.value)}
/>
{r.canConfirm && onConfirmDate && (
<Button size='small' style={{ marginTop: 4 }} disabled={draft.confirm_report_date === true} onClick={onConfirmDate}>
{draft.confirm_report_date ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}
</Button>
<Tooltip title={draft.confirm_report_date ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}>
<Button
size='small'
style={{ marginTop: 4 }}
disabled={draft.confirm_report_date === true}
aria-label={draft.confirm_report_date ? 'Дата подтверждена (до сохранения)' : 'Подтвердить дату'}
onClick={onConfirmDate}>
{draft.confirm_report_date ? 'Подтверждено' : 'Подтвердить'}
</Button>
</Tooltip>
)}
</>
) : (

View File

@ -208,7 +208,9 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
confirmDescription = confirmFailures.map((f) => <div key={`${f.column_index}-${f.relation}`}>{failureText(f)}</div>);
} else if (confirmLines.length > 0) {
confirmDescription = (
<Typography.Link onClick={() => focusRow(confirmLines[0].line_id)}>{unresolvedHint(confirmLines.length)}</Typography.Link>
<Button type='link' style={{ paddingInline: 0 }} onClick={() => focusRow(confirmLines[0].line_id)}>
{unresolvedHint(confirmLines.length)}
</Button>
);
} else if (duplicateId) {
confirmDescription = (
@ -242,9 +244,6 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
onClick={confirm}>
Подтвердить данные
</Button>
{unresolved.length > 0 && (
<Typography.Link onClick={() => focusRow(unresolved[0].key)}>{unresolvedHint(unresolved.length)}</Typography.Link>
)}
</Space>
);
@ -259,6 +258,12 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut
<div style={{ height: '100%', paddingLeft: 6 }}>
<ReviewCard title='Результат распознавания' extra={cardActions}>
<Space orientation='vertical' size={12} style={{ width: '100%' }}>
{unresolved.length > 0 && (
// настоящая кнопка: достижима Tab, срабатывает по Enter; отдельной строкой, чтобы не ломать заголовок (T11.7)
<Button type='link' style={{ paddingInline: 0 }} onClick={() => focusRow(unresolved[0].key)}>
{unresolvedHint(unresolved.length)}
</Button>
)}
<StatementHeaderBlock
header={statement.header}
documentType={statement.document.document_type}

View File

@ -40,6 +40,7 @@ type Props = {
onColumnDateConfirm?: (col: ColumnOut) => void;
};
const ICON_WIDTH = 18; // значок уровня с отступом: у ребёнка вычитается из отступа, чтобы текст не съезжал
const CHILD_INDENT = 20; // отступ названия строки детализации под родителем
function lineWeight(r: LayoutRow): number | undefined {
@ -89,7 +90,7 @@ export function StatementTable({
const rowIssues = places.byRow.get(r.key) ?? [];
const rowWorst = worstSeverity(rowIssues);
const text = (
<span style={{ fontWeight: lineWeight(r), paddingLeft: r.child ? CHILD_INDENT : undefined, display: 'inline-block' }}>
<span style={{ fontWeight: lineWeight(r), display: 'inline-block' }}>
{r.displayName || '—'}
{r.secondaryName && (
<Typography.Text type='secondary' style={{ display: 'block', fontWeight: 400, fontSize: 12 }}>
@ -111,9 +112,11 @@ export function StatementTable({
) : (
namedText
);
if (!rowWorst) return named;
// отступ ребёнка — на общей обёртке: значок уровня остаётся внутри отступа и не сдвигает текст (T11.8)
const indent = r.child ? CHILD_INDENT : undefined;
if (!rowWorst) return <div style={{ paddingLeft: indent }}>{named}</div>;
return (
<>
<div style={{ paddingLeft: indent === undefined ? undefined : indent - ICON_WIDTH, display: 'flex', alignItems: 'center' }}>
<Tooltip
title={rowIssues.map((i) => (
<div key={i.id}>{i.message}</div>
@ -123,16 +126,16 @@ export function StatementTable({
</span>
</Tooltip>
{named}
</>
</div>
);
},
},
{
key: 'code',
title: 'Код',
width: editable ? 160 : 60,
width: editable ? 160 : 84,
align: 'center' as const,
onCell: (r) => (r.kind === 'section' ? sectionSpan(r, false) : { style: { background: labelBackground(r) } }),
onCell: (r) => (r.kind === 'section' ? sectionSpan(r, false) : { style: { background: labelBackground(r), whiteSpace: 'nowrap' } }),
render: (_: unknown, r: LayoutRow) => {
if (r.kind !== 'line') return null;
const lineId = r.row.lineId;
@ -161,7 +164,7 @@ export function StatementTable({
onConfirmDate={onColumnDateConfirm}
/>
),
width: 112,
width: 128,
align: 'right' as const,
onCell: (r: LayoutRow) =>
r.kind === 'section'
@ -198,6 +201,7 @@ export function StatementTable({
return (
<Table<LayoutRow>
size='small'
tableLayout='fixed'
bordered
rowKey='key'
columns={tableColumns}

View File

@ -1,9 +1,9 @@
import { CheckOutlined, UndoOutlined } from '@ant-design/icons';
import { Button, Input, Tooltip } from 'antd';
import { Button, Input, Tooltip, Typography } from 'antd';
import type { IssueOut, ValueOut } from '../../types/api';
import { formatAmount, parseAmountInput } from '../../utils/money';
import { parseAmountInput } from '../../utils/money';
import { SeverityIcon } from './IssuesPanel';
import { cellTipLines } from './cellView';
import { cellTipLines, valueCellText } from './cellView';
import type { CellConfirmState } from './editRules';
import { worstSeverity } from './issuePlaces';
@ -51,17 +51,19 @@ export function ValueCell({
}: Props) {
const lines = cellTipLines(value, issues);
const worst = worstSeverity(issues);
// значок — в левом углу ячейки; поверх ввода и текста, не двигает суммы
const corner = worst && <SeverityIcon severity={worst} style={{ position: 'absolute', top: 1, left: 2, fontSize: 10 }} />;
const shown = valueCellText(value);
// значок уровня — внутри ячейки, слева от суммы, по центру по вертикали (T11.3)
const corner = worst && <SeverityIcon severity={worst} style={{ fontSize: 11, flex: 'none' }} />;
const tip = lines.length > 0 ? lines.map((line, i) => <div key={`${i}-${line}`}>{line}</div>) : undefined;
if (!editable) {
return (
// подсказка на всей ячейке: блок на всю ширину колонки (T11.4)
<Tooltip title={tip}>
<span>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 4, minHeight: 22 }}>
{corner}
{value.is_confirmed && <CheckOutlined style={{ fontSize: 10, color: '#8c8c8c', marginRight: 4 }} />}
{formatAmount(value.amount, { fraction: 'auto', empty: '-' })}
</span>
{value.is_confirmed && <CheckOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />}
{shown.muted ? <Typography.Text type='secondary'>{shown.text}</Typography.Text> : shown.text}
</div>
</Tooltip>
);
}
@ -70,7 +72,7 @@ export function ValueCell({
const pending = confirmState === 'pending-confirm' || confirmState === 'pending-unconfirm';
return (
<Tooltip title={invalid ? 'Неверный формат суммы' : tip}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 2, width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, width: '100%' }}>
{corner}
<Input
size='small'
@ -78,8 +80,7 @@ export function ValueCell({
prefix={value.is_confirmed ? <CheckOutlined style={{ fontSize: 10, color: '#8c8c8c' }} /> : undefined}
value={text}
onChange={(e) => onDraftChange(draftKey, e.target.value, value.amount)}
// значок записи лежит в левом углу поверх поля — освобождаем ему место, чтобы не налезал на галочку и текст
style={{ textAlign: 'right', paddingLeft: worst ? 14 : undefined }}
style={{ textAlign: 'right', minWidth: 0, flex: 1 }}
/>
{confirmState && (
<Tooltip title={CONFIRM_TITLE[confirmState]}>
@ -93,7 +94,7 @@ export function ValueCell({
/>
</Tooltip>
)}
</span>
</div>
</Tooltip>
);
}