From 6be3936d9f1ea5b0f8ed9dd73249b708f0bcec5f Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 6 Nov 2026 18:50:27 +0400 Subject: [PATCH] =?UTF-8?q?=D0=93=D1=83=D0=B4=D0=B2=D0=B8=D0=BB=D0=BB:=20?= =?UTF-8?q?=D1=81=D0=BD=D1=8F=D1=82=D0=B0=D1=8F=20=D0=B3=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B0=20=D1=81=20=D0=BD=D0=B5=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=B4=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D1=83=D0=B5=D1=82?= =?UTF-8?q?=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Финальное ревью I-1: у неотмеченной строки поле доли скрыто — effectiveRow отправляет сохранённую долю вместо неверного ввода; подсказка у «Сохранить» называет строки с ошибкой и предупреждает о скрытых фильтром. design.md goodwill-ui: R-4…R-6. Co-Authored-By: Claude Opus 5.5 --- openspec/changes/goodwill-ui/design.md | 10 ++++++++++ web/src/utils/goodwill.test.ts | 14 +++++++++++++- web/src/utils/goodwill.ts | 8 ++++++-- .../ConsolidationGoodwillContent.tsx | 12 +++++++++++- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/openspec/changes/goodwill-ui/design.md b/openspec/changes/goodwill-ui/design.md index 196dd0f..2b3544d 100644 --- a/openspec/changes/goodwill-ui/design.md +++ b/openspec/changes/goodwill-ui/design.md @@ -24,3 +24,13 @@ - **R-3 (2026-11-06). Предпросмотр — только у строк;** «Всего» и «Итого по листу» в режиме правки показывают сохранённое и обновляются после «Сохранить». Обоснование: агрегаты не считаем на фронте (R-2). Цена ошибки: аналитик видит итог после сохранения, а не во время ввода (DOC-259025 §5.1 п. 3 говорит «предварительные … итоги»). +- **R-4 (2026-11-06). `buildTree`/`rowClassName` без unit-теста.** Компоненты в проекте не тестируются (`web/CLAUDE.md`), + логика тривиальна; чистая логика вкладки (`utils/goodwill.ts`, в т. ч. `effectiveRow`) покрыта vitest. Цена ошибки: + регрессия дерева видна только глазами. +- **R-5 (2026-11-06). Проверка на стенде — частично моками.** На локальном стенде нет пар 58.01 между участниками периметра: + реальный бэк ветки проверен на пустой таблице, переключателе и экспорте, насыщенные состояния и 422 — подменой ответов + в Playwright. Цена ошибки: совпадение предпросмотра P/G с сервером на реальных данных подтверждено только тестами; после + мержа #222 пройти один сценарий на реальной паре. +- **R-6 (2026-11-06). Снятая галочка с неверной долей уходит с сохранённой долей** (финальное ревью, I-1): поле доли у + неотмеченной строки скрыто, неверный ввод там не исправить. Подсказка у «Сохранить» называет строки с ошибкой и + предупреждает, если они скрыты фильтром. Цена ошибки: неверный ввод в скрытом поле молча отбрасывается. diff --git a/web/src/utils/goodwill.test.ts b/web/src/utils/goodwill.test.ts index 505edc1..86bd8bd 100644 --- a/web/src/utils/goodwill.test.ts +++ b/web/src/utils/goodwill.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { GoodwillRow } from '../types/goodwill/goodwill'; -import { changedRows, matchesSign, parseShare, previewAmounts, rowKey } from './goodwill'; +import { changedRows, effectiveRow, matchesSign, parseShare, previewAmounts, rowKey } from './goodwill'; function row(patch: Partial = {}): GoodwillRow { return { @@ -98,3 +98,15 @@ describe('changedRows', () => { expect(changedRows([fresh], edits)[0].ownership_share).toBe('80.5'); }); }); + +describe('effectiveRow', () => { + it('снятая галочка с неверной долей уходит с сохранённой долей (ошибку у скрытого поля не исправить)', () => { + const saved = row({ ownership_share: '80.00' }); + expect(effectiveRow(saved, { include_in_kfo: false, ownership_share: '80,123' }).ownership_share).toBe('80.00'); + const fresh = row(); + expect(effectiveRow(fresh, { include_in_kfo: false, ownership_share: 'abc' }).ownership_share).toBeNull(); + }); + it('у отмеченной строки неверная доля остаётся как введена — ошибку видно у поля', () => { + expect(effectiveRow(row(), { include_in_kfo: true, ownership_share: '80,123' }).ownership_share).toBe('80,123'); + }); +}); diff --git a/web/src/utils/goodwill.ts b/web/src/utils/goodwill.ts index fcd1748..6b32a5f 100644 --- a/web/src/utils/goodwill.ts +++ b/web/src/utils/goodwill.ts @@ -58,14 +58,18 @@ export function matchesSign(goodwill: string | null, sign: GoodwillSign): boolea /** Значения строки с учётом правки: что уйдёт в пакет. */ export function effectiveRow(row: GoodwillRow, edit: GoodwillEdit | undefined): GoodwillRowKeyIn { + const include = edit?.include_in_kfo ?? row.include_in_kfo; const share = edit && 'ownership_share' in edit ? (edit.ownership_share ?? null) : row.ownership_share; const parsed = share === null ? null : parseShare(share); const comment = edit && 'comment' in edit ? (edit.comment ?? null) : row.comment; + let ownership = parsed?.ok ? parsed.value : share; + // у неотмеченной строки поле доли скрыто: неверный ввод не исправить, поэтому уходит сохранённая доля + if (!include && parsed && !parsed.ok) ownership = row.ownership_share; return { head_legal_entity_id: row.head_legal_entity.id, subsidiary_legal_entity_id: row.subsidiary_legal_entity.id, - ownership_share: parsed?.ok ? parsed.value : share, - include_in_kfo: edit?.include_in_kfo ?? row.include_in_kfo, + ownership_share: ownership, + include_in_kfo: include, comment: comment?.trim() ? comment : null, }; } diff --git a/web/src/widgets/ConsolidationGoodwill/ConsolidationGoodwillContent/ConsolidationGoodwillContent.tsx b/web/src/widgets/ConsolidationGoodwill/ConsolidationGoodwillContent/ConsolidationGoodwillContent.tsx index 2640cf2..21d0fc3 100644 --- a/web/src/widgets/ConsolidationGoodwill/ConsolidationGoodwillContent/ConsolidationGoodwillContent.tsx +++ b/web/src/widgets/ConsolidationGoodwill/ConsolidationGoodwillContent/ConsolidationGoodwillContent.tsx @@ -142,6 +142,16 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => { const filteredRows = useMemo(() => (data?.rows ?? []).filter((row) => matchesSign(row.goodwill_amount, sign)), [data, sign]); + /** Подсказка у «Сохранить»: какие строки мешают, и не скрыты ли они фильтром или настройкой колонок. */ + const issuesHint = useMemo(() => { + if (!data || !hasIssues) return undefined; + const keys = Object.keys(issues); + const names = data.rows.filter((r) => keys.includes(rowKey(r))).map((r) => r.subsidiary_legal_entity.name); + const visible = new Set(filteredRows.map(rowKey)); + const hidden = keys.some((k) => !visible.has(k)) ? ' Часть строк скрыта фильтром — сбросьте фильтр.' : ''; + return `Заполните долю и комментарий: ${names.join(', ')}.${hidden}`; + }, [data, hasIssues, issues, filteredRows]); + const closeEditing = () => { setEdits({}); setServerIssues({}); @@ -209,7 +219,7 @@ export const GoodwillContent = ({ card }: { card: VersionCard | null }) => { action={ - +