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
218 lines
7.9 KiB
TypeScript
218 lines
7.9 KiB
TypeScript
import { Table, Tooltip, Typography } from 'antd';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import type { ColumnOut } from '../../types/api';
|
||
import { CodeCell } from './CodeCell';
|
||
import { ColumnHeader } from './ColumnHeader';
|
||
import { SeverityIcon } from './IssuesPanel';
|
||
import { LineMenuButton } from './LineMenuButton';
|
||
import { ValueCell } from './ValueCell';
|
||
import { COLORS, rowBackground, valueBackground } from './cellView';
|
||
import type { CodeOption } from './codeSuggestions';
|
||
import { type CellConfirmState, cellConfirmState } from './editRules';
|
||
import type { LayoutRow } from './formLayout';
|
||
import { type IssuePlaces, worstSeverity } from './issuePlaces';
|
||
import type { LineMenuKey } from './lineMenu';
|
||
import type { LineAction, StatementDraft } from './statementDraft';
|
||
import { type GridRow, cellKey } from './statementGrid';
|
||
|
||
type Props = {
|
||
columns: ColumnOut[];
|
||
layout: LayoutRow[];
|
||
editable: boolean;
|
||
draft: Record<string, string>;
|
||
onDraftChange: (key: string, text: string, originalAmount: string | null) => void;
|
||
/** ключи ячеек/строк/колонок с ошибкой черновика (красная рамка) */
|
||
errorKeys?: Set<string>;
|
||
/** предупреждения и нарушения у мест таблицы */
|
||
places: IssuePlaces;
|
||
/** строка, которую нужно подсветить рамкой после перехода «Показать» */
|
||
highlightKey?: string | null;
|
||
/** действия над строками из черновика, подсказки кода и ошибки полей кода (lineId → текст) */
|
||
lineActions?: Record<string, LineAction>;
|
||
codeOptions?: CodeOption[];
|
||
lineErrors?: Record<string, string>;
|
||
onLineCodeChange?: (row: GridRow, code: string) => void;
|
||
onLineMenu?: (row: GridRow, key: LineMenuKey) => void;
|
||
/** весь черновик: подтверждения ячеек и даты колонок (суммы — в `draft` выше) */
|
||
fullDraft?: StatementDraft;
|
||
onConfirmToggle?: (key: string, state: NonNullable<CellConfirmState>) => void;
|
||
onColumnDateChange?: (columnId: string, text: string) => void;
|
||
onColumnDateConfirm?: (col: ColumnOut) => void;
|
||
};
|
||
|
||
const ICON_WIDTH = 18; // значок уровня с отступом: у ребёнка вычитается из отступа, чтобы текст не съезжал
|
||
const CHILD_INDENT = 20; // отступ названия строки детализации под родителем
|
||
|
||
function lineWeight(r: LayoutRow): number | undefined {
|
||
if (r.kind !== 'line') return undefined;
|
||
if (r.grand) return 700;
|
||
return r.total ? 600 : undefined;
|
||
}
|
||
const isUnresolved = (r: LayoutRow) => r.kind === 'line' && (r.row.lineStatus === 'unreadable' || r.row.lineStatus === 'missing');
|
||
// наименование и код: красноватый (нет кода) / оранжевый (ok с needsReview) / фиолетовый («БАЛАНС»)
|
||
const labelBackground = (r: LayoutRow) => (r.kind === 'line' ? (rowBackground(r.row) ?? (r.grand ? COLORS.grand : undefined)) : undefined);
|
||
|
||
export function StatementTable({
|
||
columns,
|
||
layout,
|
||
editable,
|
||
draft,
|
||
onDraftChange,
|
||
errorKeys,
|
||
places,
|
||
highlightKey = null,
|
||
lineActions = {},
|
||
codeOptions = [],
|
||
lineErrors = {},
|
||
onLineCodeChange,
|
||
onLineMenu,
|
||
fullDraft,
|
||
onConfirmToggle,
|
||
onColumnDateChange,
|
||
onColumnDateConfirm,
|
||
}: Props) {
|
||
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: COLORS.section, textAlign: 'center', fontWeight: r.level === 1 ? 700 : 600 }
|
||
: { background: labelBackground(r) },
|
||
}),
|
||
render: (_: unknown, r: LayoutRow) => {
|
||
if (r.kind === 'section') return r.title;
|
||
const rowIssues = places.byRow.get(r.key) ?? [];
|
||
const rowWorst = worstSeverity(rowIssues);
|
||
const text = (
|
||
<span style={{ fontWeight: lineWeight(r), display: 'inline-block' }}>
|
||
{r.displayName || '—'}
|
||
{r.secondaryName && (
|
||
<Typography.Text type='secondary' style={{ display: 'block', fontWeight: 400, fontSize: 12 }}>
|
||
{r.secondaryName}
|
||
</Typography.Text>
|
||
)}
|
||
</span>
|
||
);
|
||
const namedText = r.fullName ? <Tooltip title={`Распознано: ${r.fullName}`}>{text}</Tooltip> : text;
|
||
const named = editable ? (
|
||
<>
|
||
{namedText}
|
||
<LineMenuButton
|
||
row={r.row}
|
||
action={r.row.lineId ? lineActions[r.row.lineId] : undefined}
|
||
onSelect={(key) => onLineMenu?.(r.row, key)}
|
||
/>
|
||
</>
|
||
) : (
|
||
namedText
|
||
);
|
||
// отступ ребёнка — на общей обёртке: значок уровня остаётся внутри отступа и не сдвигает текст (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>
|
||
))}>
|
||
<span>
|
||
<SeverityIcon severity={rowWorst} style={{ marginRight: 4 }} />
|
||
</span>
|
||
</Tooltip>
|
||
{named}
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
key: 'code',
|
||
title: 'Код',
|
||
width: editable ? 160 : 84,
|
||
align: 'center' as const,
|
||
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;
|
||
const editing =
|
||
editable && lineId
|
||
? {
|
||
action: lineActions[lineId],
|
||
options: codeOptions,
|
||
error: lineErrors[lineId],
|
||
onCodeChange: (code: string) => onLineCodeChange?.(r.row, code),
|
||
}
|
||
: undefined;
|
||
return <CodeCell row={r.row} fontWeight={lineWeight(r)} editing={editing} />;
|
||
},
|
||
},
|
||
...columns.map((col) => ({
|
||
key: col.id,
|
||
title: (
|
||
<ColumnHeader
|
||
col={col}
|
||
issues={places.byColumn.get(col.id) ?? []}
|
||
editing={editable}
|
||
draftDate={fullDraft?.columns[col.id]}
|
||
error={errorKeys?.has(col.id)}
|
||
onDateChange={onColumnDateChange}
|
||
onConfirmDate={onColumnDateConfirm}
|
||
/>
|
||
),
|
||
width: 128,
|
||
align: 'right' as const,
|
||
onCell: (r: LayoutRow) =>
|
||
r.kind === 'section'
|
||
? sectionSpan(r, false)
|
||
: {
|
||
style: {
|
||
background: isUnresolved(r)
|
||
? rowBackground(r.row)
|
||
: (valueBackground(r.row.cells[col.id]) ?? (r.grand ? COLORS.grand : undefined)),
|
||
fontWeight: lineWeight(r),
|
||
},
|
||
},
|
||
render: (_: unknown, r: LayoutRow) => {
|
||
if (r.kind === 'section') return null;
|
||
const value = r.row.cells[col.id];
|
||
if (!value) return null;
|
||
const draftKey = cellKey(r.row.key, col.id);
|
||
return (
|
||
<ValueCell
|
||
value={value}
|
||
editable={editable}
|
||
draftKey={draftKey}
|
||
draft={draft}
|
||
onDraftChange={onDraftChange}
|
||
hasError={errorKeys?.has(draftKey)}
|
||
issues={places.byCell.get(draftKey)}
|
||
confirmState={editable && fullDraft ? cellConfirmState(r.row, value, fullDraft, draftKey) : null}
|
||
onConfirmToggle={onConfirmToggle}
|
||
/>
|
||
);
|
||
},
|
||
})),
|
||
];
|
||
return (
|
||
<Table<LayoutRow>
|
||
size='small'
|
||
tableLayout='fixed'
|
||
bordered
|
||
rowKey='key'
|
||
columns={tableColumns}
|
||
dataSource={layout}
|
||
pagination={false}
|
||
onRow={(r) =>
|
||
r.kind === 'line'
|
||
? { id: `row-${r.key}`, style: r.key === highlightKey ? { outline: '2px solid #1677ff', outlineOffset: -2 } : undefined }
|
||
: {}
|
||
}
|
||
/>
|
||
);
|
||
}
|