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:
parent
12fcea7560
commit
a3199d636e
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user