fix(stages): этап определяется по листу, направлению и названию; кодирование пути и direction в params (AURORA-1880)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-11 17:05:09 +04:00
parent 8798f092c4
commit af44b98d7a
6 changed files with 72 additions and 23 deletions

View File

@ -1,28 +1,22 @@
import api from "./client";
import { buildFormStageRequest } from "./stagesRequest";
export const StagesApi = {
createStage(formId, payload) {
return api.post(`/stages/form/${formId}`, payload).then((r) => r.data);
},
updateStage(payload) {
const { budget_form_id, sheet, phase_code, direction } = payload;
if (direction) {
return api
.patch(
`/stages/form/${budget_form_id}/${sheet}/${phase_code}?direction=${direction}`,
payload,
)
.then((r) => r.data);
}
return api
.patch(`/stages/form/${budget_form_id}/${sheet}/${phase_code}`, payload)
.then((r) => r.data);
const { path, params } = buildFormStageRequest(payload);
return api.patch(path, payload, { params }).then((r) => r.data);
},
deleteStage(formId, sheet, phaseCode) {
return api
.delete(`/stages/form/${formId}/${sheet}/${phaseCode}`)
.then((r) => r.data);
deleteStage(formId, sheet, phaseCode, direction) {
const { path, params } = buildFormStageRequest({
budget_form_id: formId,
sheet,
phase_code: phaseCode,
direction,
});
return api.delete(path, { params }).then((r) => r.data);
},
getStages(formId) {
return api.get(`/stages/form/${formId}`).then((r) => r.data);

View File

@ -0,0 +1,4 @@
export const buildFormStageRequest = ({ budget_form_id, sheet, phase_code, direction }) => ({
path: `/stages/form/${budget_form_id}/${encodeURIComponent(sheet)}/${encodeURIComponent(phase_code)}`,
params: direction ? { direction } : {},
});

View File

@ -0,0 +1,16 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { buildFormStageRequest } from './stagesRequest.js';
test('кодирует sheet и phase_code в пути', () => {
const { path } = buildFormStageRequest({ budget_form_id: 5, sheet: 'AHR', phase_code: 'А/Б?#' });
assert.equal(path, `/stages/form/5/AHR/${encodeURIComponent('А/Б?#')}`);
assert.ok(!path.slice('/stages/form/5/AHR/'.length).includes('/'));
});
test('direction уходит в params, без него params пустые', () => {
const a = buildFormStageRequest({ budget_form_id: 5, sheet: 'AHR', phase_code: 'x', direction: 'Support' });
assert.deepEqual(a.params, { direction: 'Support' });
const b = buildFormStageRequest({ budget_form_id: 5, sheet: 'OPER', phase_code: 'x', direction: null });
assert.deepEqual(b.params, {});
});

View File

@ -6,3 +6,11 @@ export const isPastDate = (value) => {
today.setHours(0, 0, 0, 0);
return date < today;
};
export const isSameStage = (a, b) =>
a.budget_form_id === b.budget_form_id &&
a.sheet === b.sheet &&
a.phase_code === b.phase_code &&
(a.direction ?? null) === (b.direction ?? null);
export const getStageKey = (stage) => [stage.sheet, stage.direction ?? '', stage.phase_code].join('|');

View File

@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { getStageKey, isSameStage } from './utils.js';
const base = { budget_form_id: 1, sheet: 'AHR', phase_code: 'Сбор', direction: 'Support' };
test('isSameStage различает Support и Development', () => {
assert.equal(isSameStage(base, { ...base, direction: 'Development' }), false);
assert.equal(isSameStage(base, { ...base }), true);
});
test('isSameStage: null и undefined direction равны', () => {
const a = { budget_form_id: 1, sheet: 'OPER', phase_code: 'X', direction: null };
const b = { budget_form_id: 1, sheet: 'OPER', phase_code: 'X' };
assert.equal(isSameStage(a, b), true);
assert.equal(isSameStage(b, a), true);
});
test('isSameStage: разные budget_form_id или phase_code - разные этапы', () => {
assert.equal(isSameStage(base, { ...base, budget_form_id: 2 }), false);
assert.equal(isSameStage(base, { ...base, phase_code: 'Другой' }), false);
assert.equal(isSameStage(base, { ...base, sheet: 'CAP' }), false);
});
test('getStageKey различается у пары Support/Development с одинаковым названием', () => {
assert.notEqual(getStageKey(base), getStageKey({ ...base, direction: 'Development' }));
assert.equal(getStageKey({ sheet: 'OPER', phase_code: 'X' }), 'OPER||X');
});

View File

@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import { toast } from 'react-toastify';
import { StagesApi } from '../api/stages';
import { isSameStage } from '../components/Stages/utils';
export const useTaskStages = (taskId) => {
const [stagesInfo, setStagesInfo] = useState({
@ -37,9 +38,7 @@ export const useTaskStages = (taskId) => {
.then((data) => {
setStagesInfo((prev) => ({
...prev,
stages: prev.stages.map((curStage) =>
curStage.sheet === stage.sheet && curStage.phase_code === stage.phase_code ? data.result : curStage,
),
stages: prev.stages.map((curStage) => (isSameStage(curStage, stage) ? data.result : curStage)),
}));
toast.success('Данные об этапе обновлены');
return data;
@ -83,14 +82,14 @@ export const useTaskStages = (taskId) => {
const deleteStage = useCallback(
(stage) => {
const { sheet, phase_code, budget_form_id } = stage;
const { sheet, phase_code, budget_form_id, direction } = stage;
if (!budget_form_id || !sheet || !phase_code) return;
StagesApi.deleteStage(budget_form_id, sheet, phase_code)
StagesApi.deleteStage(budget_form_id, sheet, phase_code, direction)
.then(() => {
setStagesInfo((prev) => ({
...prev,
stages: prev.stages.filter((curStage) => !(curStage.sheet === sheet && curStage.phase_code === phase_code)),
stages: prev.stages.filter((curStage) => !isSameStage(curStage, stage)),
}));
toast.success('Этап успешно удален');
})