feat(forms): подпись закрытого ВСП «(закрыт)» в листах форм, выбор только действующих (AURORA-1883, #330)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-12 11:49:36 +04:00
parent ca79fdedcf
commit a561939a66
6 changed files with 78 additions and 9 deletions

View File

@ -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]);

View File

@ -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
<MenuItem value='' disabled>
Выберите ВСП
</MenuItem>
{vspOptions.map((option) => (
{!currentIsSelectable && (
<MenuItem value={value} disabled sx={{ display: 'none' }}>
{getVspLabel(value, vspOptions)}
</MenuItem>
)}
{selectableOptions.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.registration_number}
</MenuItem>

View File

@ -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(() => {

View File

@ -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);
}

View File

@ -0,0 +1,17 @@
// AURORA-1883: подпись ВСП в ячейке листа формы (колонки editType 'vsp_dropdown').
// vspOptions — ответ /dict/info/dropdown?include_inactive=true: действующие и закрытые ВСП ССП формы.
const isEmptyValue = (value) => value === null || value === undefined || value === '';
/** Рег. номер ВСП; у закрытого — «<рег. номер> (закрыт)»; не найден — «ВСП #<id>»; пусто — 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);
}

View File

@ -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 "ВСП #<id>", 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), []);
});