fix(vsp): архивные ССП в фильтре и выгрузке для «Закрытые»/«Все» — закрытые ВСП архивного ССП выгружаются (AURORA-1883, #330, I-1)

Окно создания и правки ВСП по-прежнему предлагает только активные ССП.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-12 12:28:04 +04:00
parent f1fca5d12c
commit a4a674fe60
4 changed files with 46 additions and 7 deletions

View File

@ -13,7 +13,7 @@ import { PageContainer } from '../../components/common/PageContainer';
import DictVspFilters from './components/DictVspFilters'; import DictVspFilters from './components/DictVspFilters';
import DictVspModals from './components/DictVspModals'; import DictVspModals from './components/DictVspModals';
import TableDictVsp from './components/TableDictVsp'; import TableDictVsp from './components/TableDictVsp';
import { buildVspListParams, vspMatchesStatus } from './utils/vspListParams'; import { buildVspListParams, sspsForStatus, vspMatchesStatus } from './utils/vspListParams';
const DictVspInfo = () => { const DictVspInfo = () => {
const portalContent = ( const portalContent = (
@ -40,6 +40,16 @@ function DictVspPage() {
const modalsRef = useRef(null); const modalsRef = useRef(null);
const listFilter = useMemo(() => ({ status, ...closeDates }), [status, closeDates]); const listFilter = useMemo(() => ({ status, ...closeDates }), [status, closeDates]);
const visibleSsps = useMemo(() => sspsForStatus(ssps, status), [ssps, status]);
// Создание и правка ВСП — только на активном ССП (бэк отклоняет архивный).
const activeSsps = useMemo(() => sspsForStatus(ssps, 'active'), [ssps]);
// Выбранный архивный ССП исчезает из фильтра при возврате в «Действующие» — сбрасываем выбор.
useEffect(() => {
if (selectedSsp && !visibleSsps.some((ssp) => ssp.id === selectedSsp.id)) {
setSelectedSsp(null);
}
}, [selectedSsp, visibleSsps]);
const loadVspList = useCallback(async () => { const loadVspList = useCallback(async () => {
if (!user) return; if (!user) return;
@ -70,7 +80,8 @@ function DictVspPage() {
setIsLoading(true); setIsLoading(true);
try { try {
const isAdmin = user.role_id === ROLES_NAME_ID.admin; const isAdmin = user.role_id === ROLES_NAME_ID.admin;
const sspResponse = isAdmin ? await SspApi.getAll({ is_active: true }) : null; // AURORA-1883: все ССП, включая архивные, — для закрытых ВСП; в «Действующих» архивные скрываются (sspsForStatus).
const sspResponse = isAdmin ? await SspApi.getAll() : null;
if (isAdmin) { if (isAdmin) {
if (!sspResponse.success) { if (!sspResponse.success) {
@ -176,7 +187,7 @@ function DictVspPage() {
<Stack sx={{ alignItems: 'stretch', width: '100%' }}> <Stack sx={{ alignItems: 'stretch', width: '100%' }}>
<Box sx={{ width: '100%' }}> <Box sx={{ width: '100%' }}>
<DictVspFilters <DictVspFilters
ssps={ssps} ssps={visibleSsps}
onSspChange={setSelectedSsp} onSspChange={setSelectedSsp}
selectedSsp={selectedSsp} selectedSsp={selectedSsp}
showSsp={user.role_id !== ROLES_NAME_ID.executor_rf} showSsp={user.role_id !== ROLES_NAME_ID.executor_rf}
@ -196,7 +207,8 @@ function DictVspPage() {
/> />
<DictVspModals <DictVspModals
ref={modalsRef} ref={modalsRef}
ssps={ssps} ssps={activeSsps}
exportSsps={visibleSsps}
onAdd={handleAdd} onAdd={handleAdd}
onEdit={handleEdit} onEdit={handleEdit}
onDelete={handleDelete} onDelete={handleDelete}

View File

@ -3,7 +3,7 @@ import ModalEditAddVsp from './Modals/ModalEditAddVsp';
import ModalExport from './Modals/ModalExport'; import ModalExport from './Modals/ModalExport';
// Состояние модалок живёт здесь, а не в таблице: открытие/закрытие не перерисовывает MRT. // Состояние модалок живёт здесь, а не в таблице: открытие/закрытие не перерисовывает MRT.
const DictVspModals = forwardRef(({ onAdd, onEdit, onDelete, ssps, listFilter }, ref) => { const DictVspModals = forwardRef(({ onAdd, onEdit, onDelete, ssps, exportSsps, listFilter }, ref) => {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [modalExportOpen, setModalExportOpen] = useState(false); const [modalExportOpen, setModalExportOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null); const [editingItem, setEditingItem] = useState(null);
@ -55,7 +55,7 @@ const DictVspModals = forwardRef(({ onAdd, onEdit, onDelete, ssps, listFilter },
loading={saveLoading} loading={saveLoading}
ssps={ssps} ssps={ssps}
/> />
<ModalExport open={modalExportOpen} onClose={closeExport} ssps={ssps} listFilter={listFilter} /> <ModalExport open={modalExportOpen} onClose={closeExport} ssps={exportSsps ?? ssps} listFilter={listFilter} />
</> </>
); );
}); });

View File

@ -21,3 +21,9 @@ export function vspMatchesStatus(vsp, status) {
if (status === 'closed') return vsp.is_active === false; if (status === 'closed') return vsp.is_active === false;
return true; return true;
} }
/** ССП для фильтра и выгрузки: архивные — только при «Закрытые»/«Все» (закрытые ВСП бывают на архивных ССП). */
export function sspsForStatus(ssps, status) {
const list = ssps || [];
return status === 'active' ? list.filter((ssp) => ssp.is_active !== false) : list;
}

View File

@ -1,6 +1,6 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { buildVspListParams, vspMatchesStatus } from './vspListParams.js'; import { buildVspListParams, sspsForStatus, vspMatchesStatus } from './vspListParams.js';
// AURORA-1883: параметры запроса справочника ВСП по статусу и периоду закрытия. // AURORA-1883: параметры запроса справочника ВСП по статусу и периоду закрытия.
test('active status sends no close period', () => { test('active status sends no close period', () => {
@ -31,3 +31,24 @@ test('vspMatchesStatus follows is_active', () => {
assert.equal(vspMatchesStatus({ is_active: true }, 'closed'), false); assert.equal(vspMatchesStatus({ is_active: true }, 'closed'), false);
assert.equal(vspMatchesStatus({ is_active: false }, 'all'), true); assert.equal(vspMatchesStatus({ is_active: false }, 'all'), true);
}); });
// AURORA-1883, финальное ревью I-1: закрытые ВСП архивного ССП должны фильтроваться и выгружаться.
test('sspsForStatus hides archived SSPs only for active VSPs', () => {
const ssps = [
{ id: 1, is_active: true },
{ id: 2, is_active: false },
];
assert.deepEqual(
sspsForStatus(ssps, 'active').map((s) => s.id),
[1],
);
assert.deepEqual(
sspsForStatus(ssps, 'closed').map((s) => s.id),
[1, 2],
);
assert.deepEqual(
sspsForStatus(ssps, 'all').map((s) => s.id),
[1, 2],
);
assert.deepEqual(sspsForStatus(undefined, 'active'), []);
});