From 14693d8dce678ccb9b58f60fdb5fdb929f0c60ff Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:26:28 +0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20Task?= =?UTF-8?q?=204=20=E2=80=94=20=D0=BF=D0=BE=D0=B4=D1=81=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D0=BA=D0=B8,=20=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F,=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20(#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/utils/consolidations.test.ts | 9 +++++++ .../CorrectionTable/CorrectionContent.tsx | 8 +++++- .../CorrectionTable/CorrectionTable.tsx | 27 ++++++++++++++----- .../widgets/CorrectionTable/HeaderFilter.tsx | 4 +-- .../CorrectionTable/useCorrectionTable.ts | 9 ++++--- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/web/src/utils/consolidations.test.ts b/web/src/utils/consolidations.test.ts index 9dd568e..240bc2d 100644 --- a/web/src/utils/consolidations.test.ts +++ b/web/src/utils/consolidations.test.ts @@ -281,6 +281,15 @@ describe('resolveLeaveAfterFetch (T7.3)', () => { }); }); +describe('reportTabNavigation: вкладка «Таблица корректировки» (#206, T4.13)', () => { + it('шахматка → таблица корректировки: меняется последний сегмент, запись истории заменяется', () => { + expect(reportTabNavigation('/groups/g/versions/v/chess', 'chess', 'correction')).toEqual({ + to: '/groups/g/versions/v/correction', + options: { replace: true }, + }); + }); +}); + describe('reportTabNavigation: адрес без search (T7.4)', () => { it('?holder=&missing= не переносятся на другую вкладку', () => { expect(reportTabNavigation('/groups/g/versions/v/osv', 'osv', 'chess').to).toBe('/groups/g/versions/v/chess'); diff --git a/web/src/widgets/CorrectionTable/CorrectionContent.tsx b/web/src/widgets/CorrectionTable/CorrectionContent.tsx index e19c371..29fc5ef 100644 --- a/web/src/widgets/CorrectionTable/CorrectionContent.tsx +++ b/web/src/widgets/CorrectionTable/CorrectionContent.tsx @@ -28,6 +28,9 @@ function readOnlyHint(t: CorrectionTableOut): string | null { return READ_ONLY_HINTS[t.read_only_reason]; } +/** Решения по строкам и окно корректировки подключаются в Task 5–6: тогда флаг становится true (одна правка). */ +const ROW_ACTIONS_READY = false; + const LOAD_ERROR_KEY = 'corrections-load-error'; type Props = { versionId: string; card?: VersionCard | null }; @@ -61,6 +64,9 @@ export function CorrectionContent({ versionId, card = null }: Props) { }); }, [error, notification, reload]); + // ушли с вкладки — уведомление об ошибке закрываем, оно относится к этому экрану + useEffect(() => () => notification.destroy(LOAD_ERROR_KEY), [notification]); + useEffect(() => { const onChange = () => { if (!document.fullscreenElement) setFullscreen(false); @@ -134,7 +140,7 @@ export function CorrectionContent({ versionId, card = null }: Props) { rules={table.rules} editable={table.editable} readOnlyHint={readOnlyHint(table)} - actionsEnabled={false} + actionsEnabled={ROW_ACTIONS_READY && table.editable} actions={actions} filters={filters} onFiltersChange={setFilters} diff --git a/web/src/widgets/CorrectionTable/CorrectionTable.tsx b/web/src/widgets/CorrectionTable/CorrectionTable.tsx index ea09a8b..bc20665 100644 --- a/web/src/widgets/CorrectionTable/CorrectionTable.tsx +++ b/web/src/widgets/CorrectionTable/CorrectionTable.tsx @@ -57,6 +57,8 @@ const BG_CHANGED = '#fff6e5'; /** «394.27» → «394,27%»; строка не переводится в число. */ const percentText = (p: string | null): string => (p === null ? '—' : `${p.replace('.', ',')}%`); +const UNAVAILABLE_HINT = 'Недоступно в этой версии экрана'; + const showLine = (line: string): string => articleCodes(line).join(' / '); const sideSuffix = (row: TableRow) => { @@ -106,15 +108,26 @@ export function CorrectionTable(props: Props) { else if (key === 'reset') actions.onAllocate({ ...base, action: 'RESET' }); }; - const withHint = (node: ReactNode) => (readOnlyHint ? {node} : node); + // почему контрол неактивен: сервер закрыл правки — его причина; иначе действие ещё не подключено в этой версии экрана + const inactiveHint = readOnlyHint ?? (actionsEnabled ? null : UNAVAILABLE_HINT); + const withHint = (node: ReactNode, hint: string | null = inactiveHint) => + hint ? ( + + {node} + + ) : ( + node + ); const lines = (list: CorrectionLineOut[] | undefined, pick: (l: CorrectionLineOut) => string): ReactNode => - list && list.length > 0 ? list.map((l) =>
{pick(l)}
) : '—'; + list && list.length > 0 ? list.map((l, i) =>
{pick(l)}
) : '—'; const cell = (key: ColKey, row: TableRow): ReactNode => { if (row.kind === 'aggregate') { const a = row.aggregate; - const red = a.exceeds_threshold ? { color: COLOR_OVER } : undefined; + // красным — только значение выше порога; прочерк («нет значения») не красим + const redIf = (text: string): ReactNode => + a.exceeds_threshold && text !== '—' ? {text} : text; switch (key) { case 'include': return withHint( @@ -127,6 +140,7 @@ export function CorrectionTable(props: Props) { )} , + readOnlyHint, ); case 'd_entity': return a.company_1.name; @@ -145,9 +159,9 @@ export function CorrectionTable(props: Props) { case 'k_amount': return format(a.side_2.amount); case 'dev_percent': - return {percentText(a.deviation_percent)}; + return redIf(percentText(a.deviation_percent)); case 'dev_amount': - return {format(a.deviation_amount)}; + return redIf(format(a.deviation_amount)); case 'c_d_line': return lines(a.correction?.debit, (l) => l.line_code); case 'c_d_amount': @@ -254,7 +268,7 @@ export function CorrectionTable(props: Props) { width, ellipsis, render: (_, row) => cell(key, row), - onCell: (row) => ({ style: { background: rowBg(row) ?? tint(key), opacity: row.kind === 'entry' && row.entry.excluded ? 0.5 : 1 } }), + onCell: (row) => ({ style: { background: rowBg(row) ?? tint(key), opacity: (row.kind === 'entry' || row.kind === 'unallocated') && row.entry.excluded ? 0.5 : 1 } }), }); const filtersTitle = (label: string, active: boolean, body: ReactNode) => ( @@ -323,6 +337,7 @@ export function CorrectionTable(props: Props) { type='text' size='small' aria-label={expanded.has(row.key) ? 'Свернуть' : 'Раскрыть'} + aria-expanded={expanded.has(row.key)} icon={expanded.has(row.key) ? : } onClick={() => onToggle(row.key)} /> diff --git a/web/src/widgets/CorrectionTable/HeaderFilter.tsx b/web/src/widgets/CorrectionTable/HeaderFilter.tsx index 0f4d2e6..1631b07 100644 --- a/web/src/widgets/CorrectionTable/HeaderFilter.tsx +++ b/web/src/widgets/CorrectionTable/HeaderFilter.tsx @@ -3,7 +3,7 @@ import { Input, Popover, Select } from 'antd'; import type { ReactNode } from 'react'; type Props = { - label: ReactNode; + label: string; /** Фильтр задан — значок выделяется. */ active: boolean; children: ReactNode; @@ -18,7 +18,7 @@ export function HeaderFilter({ label, active, children }: Props) { {/* стопим всплытие, чтобы клик не запускал выбор заголовка */}