From a561939a6615be1ca64b1fe7d44ef2f80ef30240 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 12 Nov 2026 11:49:36 +0400 Subject: [PATCH] =?UTF-8?q?feat(forms):=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D1=8C=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=92=D0=A1=D0=9F=20=C2=AB(=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82)=C2=BB=20=D0=B2=20=D0=BB=D0=B8=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=85=20=D1=84=D0=BE=D1=80=D0=BC,=20=D0=B2=D1=8B=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D1=85=20(AUROR?= =?UTF-8?q?A-1883,=20#330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../RealtimeTable/Cell/Cell/Cell.jsx | 12 +++--- .../Cell/EditCell/VspDropdownEditCell.jsx | 14 ++++++- .../RealtimeTable/Modals/SelectVspModal.jsx | 1 + .../RealtimeTable/RealtimeTable.jsx | 3 +- .../RealtimeTable/utils/vspLabel.js | 17 ++++++++ .../RealtimeTable/utils/vspLabel.test.js | 40 +++++++++++++++++++ 6 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 web/src/components/RealtimeTable/utils/vspLabel.js create mode 100644 web/src/components/RealtimeTable/utils/vspLabel.test.js diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx index bd07a07..d55ed56 100644 --- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx +++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx @@ -4,6 +4,7 @@ import { useCellLock } from '../../contexts/RealtimeContext'; import { resolveCellTextColor } from '../../utils/cellTextColor'; import { formatNumber } from '../../utils/numberFormat'; import { isTextColumn } from '../../utils/moneyUtils'; +import { getVspLabel } from '../../utils/vspLabel'; const BASE_CELL_STYLES = { width: '100%', @@ -164,6 +165,10 @@ const CellComponent = ({ const { rawValue, displayValue, isNumeric, isNegative } = useMemo(() => { const value = cell.getValue(); + // AURORA-1883: id ВСП — не сумма; подпись (рег. номер, «(закрыт)», «ВСП #id») без разрядности. + if (isVspDropdown) { + return { rawValue: value, displayValue: getVspLabel(value, vspOptions) ?? '', isNumeric: false, isNegative: false }; + } const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value)) && !isTextColumn(column); let display = String(value || ''); let negative = false; @@ -173,13 +178,6 @@ const CellComponent = ({ negative = formatted.isNegative; } - if (isVspDropdown && value != null) { - const vsp = vspOptions?.find((v) => v.id === Number(value)); - if (vsp) { - return { rawValue: value, displayValue: vsp.registration_number, isNumeric: false, isNegative: false }; - } - } - return { rawValue: value, displayValue: display, isNumeric: isNum, isNegative: negative }; }, [cell, column.id, isVspDropdown, vspOptions, displayFactor]); diff --git a/web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx b/web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx index beafa61..b233dfd 100644 --- a/web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx +++ b/web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx @@ -1,6 +1,7 @@ import { FormControl, MenuItem, Select } from '@mui/material'; import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useRealtimeActions, useVspOptions } from '../../contexts/RealtimeContext'; +import { getVspLabel, selectableVspOptions } from '../../utils/vspLabel'; const EDITOR_STYLES = { position: 'absolute', @@ -11,6 +12,12 @@ const EDITOR_STYLES = { const VspDropdownEditCell = memo(({ refCell, onChange, disabled, value, table, cell }) => { const { endEditing: contextEndEditing } = useRealtimeActions(); const vspOptions = useVspOptions(); + // AURORA-1883: выбрать можно только действующий ВСП; закрытый текущий — подписан, но не выбирается. + const selectableOptions = useMemo(() => selectableVspOptions(vspOptions), [vspOptions]); + const currentIsSelectable = useMemo( + () => value == null || value === '' || selectableOptions.some((option) => Number(option.id) === Number(value)), + [selectableOptions, value], + ); const [selectedValue, setSelectedValue] = useState(() => value ?? ''); const [position, setPosition] = useState(); @@ -140,7 +147,12 @@ const VspDropdownEditCell = memo(({ refCell, onChange, disabled, value, table, c Выберите ВСП - {vspOptions.map((option) => ( + {!currentIsSelectable && ( + + {getVspLabel(value, vspOptions)} + + )} + {selectableOptions.map((option) => ( {option.registration_number} diff --git a/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx b/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx index 88cd4da..c3d4043 100644 --- a/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx +++ b/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx @@ -8,6 +8,7 @@ import { Cancel } from '../../common/icons/icons'; const SelectVspModalComponent = ({ isOpen, onClose, onSelect, formId, title = 'Выбор ВСП', isSaving = false }) => { const [selectedValue, setSelectedValue] = useState(''); + // AURORA-1883: модалка сама грузит dropdown без include_inactive — в выборе только действующие ВСП. const [vspOptions, setVspOption] = useState([]); useEffect(() => { diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 93e456d..d4e442d 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -308,7 +308,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { if (!hasVspDropdown(columnsConfig.columns)) return; let isCancelled = false; - DictVspApi.getDropdownVsp({ form_id: formId }).then((data) => { + // AURORA-1883: с закрытыми — для подписей строк на закрытых ВСП; выбор ограничен в VspDropdownEditCell. + DictVspApi.getDropdownVsp({ form_id: formId, include_inactive: true }).then((data) => { if (!isCancelled && data.success) { setVspOptions(data.result); } diff --git a/web/src/components/RealtimeTable/utils/vspLabel.js b/web/src/components/RealtimeTable/utils/vspLabel.js new file mode 100644 index 0000000..4dd7f8d --- /dev/null +++ b/web/src/components/RealtimeTable/utils/vspLabel.js @@ -0,0 +1,17 @@ +// AURORA-1883: подпись ВСП в ячейке листа формы (колонки editType 'vsp_dropdown'). +// vspOptions — ответ /dict/info/dropdown?include_inactive=true: действующие и закрытые ВСП ССП формы. + +const isEmptyValue = (value) => value === null || value === undefined || value === ''; + +/** Рег. номер ВСП; у закрытого — «<рег. номер> (закрыт)»; не найден — «ВСП #»; пусто — null. */ +export function getVspLabel(value, vspOptions) { + if (isEmptyValue(value)) return null; + const vsp = (vspOptions || []).find((option) => Number(option.id) === Number(value)); + if (!vsp) return `ВСП #${value}`; + return vsp.is_active === false ? `${vsp.registration_number} (закрыт)` : vsp.registration_number; +} + +/** ВСП, которые можно выбрать для строки: только действующие. */ +export function selectableVspOptions(vspOptions) { + return (vspOptions || []).filter((option) => option.is_active !== false); +} diff --git a/web/src/components/RealtimeTable/utils/vspLabel.test.js b/web/src/components/RealtimeTable/utils/vspLabel.test.js new file mode 100644 index 0000000..1200386 --- /dev/null +++ b/web/src/components/RealtimeTable/utils/vspLabel.test.js @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { getVspLabel, selectableVspOptions } from './vspLabel.js'; + +// AURORA-1883: в листах форм закрытый ВСП подписан рег. номером с пометкой, а не числом. +const opts = [ + { id: 1, registration_number: '1234/567', is_active: true }, + { id: 2, registration_number: '7777/001', is_active: false }, +]; + +test('active VSP is labelled by its registration number', () => { + assert.equal(getVspLabel(1, opts), '1234/567'); +}); + +test('closed VSP is labelled with the "(закрыт)" suffix', () => { + assert.equal(getVspLabel(2, opts), '7777/001 (закрыт)'); +}); + +test('string id is matched like a number', () => { + assert.equal(getVspLabel('1', opts), '1234/567'); +}); + +test('unknown id is labelled "ВСП #", never formatted as a number', () => { + assert.equal(getVspLabel(99, opts), 'ВСП #99'); + assert.equal(getVspLabel(5, undefined), 'ВСП #5'); +}); + +test('empty value has no label', () => { + assert.equal(getVspLabel(null, opts), null); + assert.equal(getVspLabel(undefined, opts), null); + assert.equal(getVspLabel('', opts), null); +}); + +test('only active VSPs are selectable', () => { + assert.deepEqual( + selectableVspOptions(opts).map((v) => v.id), + [1], + ); + assert.deepEqual(selectableVspOptions(undefined), []); +});