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 && (
+
+ )}
+ {selectableOptions.map((option) => (
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), []);
+});