From a779d2a04a7c3395066fc4444ffee0a7cc1e3e41 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 13:39:34 +0400 Subject: [PATCH] =?UTF-8?q?feat(statement):=20=D0=BF=D0=BE=D0=B4=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=8F?= =?UTF-8?q?=D1=87=D0=B5=D0=B5=D0=BA,=20=D0=B4=D0=B0=D1=82=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=BD=D0=BA=D0=B8,=20=D0=BD=D0=B0=D0=B8?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BE?= =?UTF-8?q?=D1=80=D0=B3=D0=B0=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B0=D1=82=D1=8B=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W --- src/widgets/DocumentReview/ColumnHeader.tsx | 60 +++++++++++++++++++ .../DocumentReview/StatementHeaderBlock.tsx | 4 +- .../DocumentReview/StatementReview.tsx | 14 +++++ src/widgets/DocumentReview/StatementTable.tsx | 46 +++++++------- src/widgets/DocumentReview/ValueCell.tsx | 45 ++++++++++++-- src/widgets/DocumentReview/editRules.test.ts | 59 ++++++++++++++++++ src/widgets/DocumentReview/editRules.ts | 25 ++++++++ .../DocumentReview/statementDraft.test.ts | 13 ++++ src/widgets/DocumentReview/statementDraft.ts | 3 +- 9 files changed, 241 insertions(+), 28 deletions(-) create mode 100644 src/widgets/DocumentReview/ColumnHeader.tsx create mode 100644 src/widgets/DocumentReview/editRules.test.ts create mode 100644 src/widgets/DocumentReview/editRules.ts diff --git a/src/widgets/DocumentReview/ColumnHeader.tsx b/src/widgets/DocumentReview/ColumnHeader.tsx new file mode 100644 index 0000000..7c44e3f --- /dev/null +++ b/src/widgets/DocumentReview/ColumnHeader.tsx @@ -0,0 +1,60 @@ +import { Button, Input, Tooltip } from 'antd'; +import type { ColumnOut, IssueOut } from '../../types/api'; +import { SeverityIcon } from './IssuesPanel'; +import { columnDateEditable } from './editRules'; +import { worstSeverity } from './issuePlaces'; +import { columnTitle } from './reviewView'; + +type Props = { + col: ColumnOut; + issues: IssueOut[]; + editing: boolean; + /** введённая, но не сохранённая дата колонки */ + draftDate?: string; + error?: boolean; + onDateChange?: (columnId: string, text: string) => void; + /** «Подтвердить дату»: кладёт в черновик текущую дату колонки */ + onConfirmDate?: (col: ColumnOut) => void; +}; + +/** Заголовок колонки: подпись, значок предупреждения и (в режиме правки) поле даты ГГГГ-ММ-ДД с подтверждением. */ +export function ColumnHeader({ col, issues, editing, draftDate, error = false, onDateChange, onConfirmDate }: Props) { + const worst = worstSeverity(issues); + const title = ( + + {worst && } + {columnTitle(col)} + + ); + const withTip = worst ? ( + ( +
{i.message}
+ ))}> + {title} +
+ ) : ( + title + ); + const { editable, canConfirm } = columnDateEditable(col, issues); + if (!editing || !editable) return withTip; + const pending = canConfirm && draftDate !== undefined && draftDate === col.period_date; + return ( +
+ {withTip} + onDateChange?.(col.id, e.target.value)} + style={{ marginTop: 4, fontWeight: 400 }} + /> + {canConfirm && ( + + )} +
+ ); +} diff --git a/src/widgets/DocumentReview/StatementHeaderBlock.tsx b/src/widgets/DocumentReview/StatementHeaderBlock.tsx index c4411a1..950fb88 100644 --- a/src/widgets/DocumentReview/StatementHeaderBlock.tsx +++ b/src/widgets/DocumentReview/StatementHeaderBlock.tsx @@ -73,8 +73,8 @@ export function StatementHeaderBlock({ header, documentType, editing, draft, onD onChange={(e) => onDraftChange(r.field, e.target.value)} /> {r.canConfirm && onConfirmDate && ( - )} diff --git a/src/widgets/DocumentReview/StatementReview.tsx b/src/widgets/DocumentReview/StatementReview.tsx index 5fd177b..cb21607 100644 --- a/src/widgets/DocumentReview/StatementReview.tsx +++ b/src/widgets/DocumentReview/StatementReview.tsx @@ -273,6 +273,20 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut } onLineMenu={(row, key) => setDraft((prev) => ({ ...prev, lines: applyLineMenu(row, key, prev.lines) }))} errorKeys={errorKeys} + fullDraft={draft} + onConfirmToggle={(key, state) => + setDraft((prev) => { + const confirms = { ...prev.confirms }; + // повторный клик по нажатой кнопке убирает подтверждение из черновика + if (state === 'pending-confirm' || state === 'pending-unconfirm') delete confirms[key]; + else confirms[key] = state === 'confirm'; + return { ...prev, confirms }; + }) + } + onColumnDateChange={(columnId, text) => setDraft((prev) => ({ ...prev, columns: { ...prev.columns, [columnId]: text } }))} + onColumnDateConfirm={(col) => + col.period_date && setDraft((prev) => ({ ...prev, columns: { ...prev.columns, [col.id]: col.period_date as string } })) + } onDraftChange={(key, text, original) => setDraft((prev) => ({ ...prev, amounts: applyDraftInput(prev.amounts, key, text, original, parseAmountInput) })) } diff --git a/src/widgets/DocumentReview/StatementTable.tsx b/src/widgets/DocumentReview/StatementTable.tsx index 629c9cf..febd274 100644 --- a/src/widgets/DocumentReview/StatementTable.tsx +++ b/src/widgets/DocumentReview/StatementTable.tsx @@ -1,17 +1,18 @@ import { Table, Tooltip, Typography } from 'antd'; import type { ColumnsType } from 'antd/es/table'; -import type { ColumnOut, IssueOut } from '../../types/api'; +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 { columnTitle } from './reviewView'; -import type { LineAction } from './statementDraft'; +import type { LineAction, StatementDraft } from './statementDraft'; import { type GridRow, cellKey } from './statementGrid'; type Props = { @@ -32,6 +33,11 @@ type Props = { lineErrors?: Record; onLineCodeChange?: (row: GridRow, code: string) => void; onLineMenu?: (row: GridRow, key: LineMenuKey) => void; + /** весь черновик: подтверждения ячеек и даты колонок (суммы — в `draft` выше) */ + fullDraft?: StatementDraft; + onConfirmToggle?: (key: string, state: NonNullable) => void; + onColumnDateChange?: (columnId: string, text: string) => void; + onColumnDateConfirm?: (col: ColumnOut) => void; }; const CHILD_INDENT = 20; // отступ названия строки детализации под родителем @@ -45,22 +51,6 @@ const isUnresolved = (r: LayoutRow) => r.kind === 'line' && r.row.lineStatus !== // наименование и код: красноватый (нет кода) / оранжевый (ok с needsReview) / фиолетовый («БАЛАНС») const labelBackground = (r: LayoutRow) => (r.kind === 'line' ? (rowBackground(r.row) ?? (r.grand ? COLORS.grand : undefined)) : undefined); -function ColumnTitle({ col, issues }: { col: ColumnOut; issues: IssueOut[] }) { - const worst = worstSeverity(issues); - if (!worst) return <>{columnTitle(col)}; - return ( - ( -
{i.message}
- ))}> - - - {columnTitle(col)} - -
- ); -} - export function StatementTable({ columns, layout, @@ -75,6 +65,10 @@ export function StatementTable({ lineErrors = {}, onLineCodeChange, onLineMenu, + fullDraft, + onConfirmToggle, + onColumnDateChange, + onColumnDateConfirm, }: Props) { const span = columns.length + 2; // строка-заголовок раздела занимает всю ширину: первая ячейка colSpan = все колонки, остальные скрыты @@ -156,7 +150,17 @@ export function StatementTable({ }, ...columns.map((col) => ({ key: col.id, - title: , + title: ( + + ), width: 112, align: 'right' as const, onCell: (r: LayoutRow) => @@ -184,6 +188,8 @@ export function StatementTable({ onDraftChange={onDraftChange} hasError={errorKeys?.has(draftKey)} issues={places.byCell.get(draftKey)} + confirmState={editable && fullDraft ? cellConfirmState(r.row, value, fullDraft, draftKey) : null} + onConfirmToggle={onConfirmToggle} /> ); }, diff --git a/src/widgets/DocumentReview/ValueCell.tsx b/src/widgets/DocumentReview/ValueCell.tsx index e1e8ce4..c86a057 100644 --- a/src/widgets/DocumentReview/ValueCell.tsx +++ b/src/widgets/DocumentReview/ValueCell.tsx @@ -1,9 +1,10 @@ -import { CheckOutlined } from '@ant-design/icons'; -import { Input, Tooltip } from 'antd'; +import { CheckOutlined, UndoOutlined } from '@ant-design/icons'; +import { Button, Input, Tooltip } from 'antd'; import type { IssueOut, ValueOut } from '../../types/api'; import { formatAmount, parseAmountInput } from '../../utils/money'; import { SeverityIcon } from './IssuesPanel'; import { cellTipLines } from './cellView'; +import type { CellConfirmState } from './editRules'; import { worstSeverity } from './issuePlaces'; type Props = { @@ -17,10 +18,30 @@ type Props = { issues?: IssueOut[]; /** ошибка черновика, найденная при сохранении */ hasError?: boolean; + /** какая кнопка подтверждения нужна ячейке (editRules.cellConfirmState) и обработчик её нажатия */ + confirmState?: CellConfirmState; + onConfirmToggle?: (key: string, state: NonNullable) => void; }; +const CONFIRM_TITLE = { + confirm: 'Подтвердить значение', + unconfirm: 'Снять подтверждение', + 'pending-confirm': 'Подтверждение будет сохранено — нажмите, чтобы убрать', + 'pending-unconfirm': 'Снятие подтверждения будет сохранено — нажмите, чтобы убрать', +} as const; + /** Ячейка суммы: просмотр (с серой галочкой у подтверждённой) или поле ввода. */ -export function ValueCell({ value, editable, draftKey, draft, onDraftChange, issues = [], hasError = false }: Props) { +export function ValueCell({ + value, + editable, + draftKey, + draft, + onDraftChange, + issues = [], + hasError = false, + confirmState = null, + onConfirmToggle, +}: Props) { const lines = cellTipLines(value, issues); const worst = worstSeverity(issues); // значок — в левом углу ячейки; поверх ввода и текста, не двигает суммы @@ -39,9 +60,10 @@ export function ValueCell({ value, editable, draftKey, draft, onDraftChange, iss } const text = draft[draftKey] ?? value.amount ?? ''; const invalid = hasError || (draftKey in draft && !parseAmountInput(text).ok); + const pending = confirmState === 'pending-confirm' || confirmState === 'pending-unconfirm'; return ( - + {corner} : undefined} value={text} onChange={(e) => onDraftChange(draftKey, e.target.value, value.amount)} - style={{ textAlign: 'right' }} + // значок записи лежит в левом углу поверх поля — освобождаем ему место, чтобы не налезал на галочку и текст + style={{ textAlign: 'right', paddingLeft: worst ? 14 : undefined }} /> + {confirmState && ( + +