diff --git a/web/src/api/stages.js b/web/src/api/stages.js index 1049600..c33be3f 100644 --- a/web/src/api/stages.js +++ b/web/src/api/stages.js @@ -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); diff --git a/web/src/api/stagesRequest.js b/web/src/api/stagesRequest.js new file mode 100644 index 0000000..9a95302 --- /dev/null +++ b/web/src/api/stagesRequest.js @@ -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 } : {}, +}); diff --git a/web/src/api/stagesRequest.test.js b/web/src/api/stagesRequest.test.js new file mode 100644 index 0000000..5c5b1b6 --- /dev/null +++ b/web/src/api/stagesRequest.test.js @@ -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, {}); +}); diff --git a/web/src/components/Stages/utils.js b/web/src/components/Stages/utils.js index c4aaae9..7936ca3 100644 --- a/web/src/components/Stages/utils.js +++ b/web/src/components/Stages/utils.js @@ -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('|'); diff --git a/web/src/components/Stages/utils.test.js b/web/src/components/Stages/utils.test.js new file mode 100644 index 0000000..c190fc5 --- /dev/null +++ b/web/src/components/Stages/utils.test.js @@ -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'); +}); diff --git a/web/src/hooks/useTaskStages.js b/web/src/hooks/useTaskStages.js index 84400f3..9bcae5a 100644 --- a/web/src/hooks/useTaskStages.js +++ b/web/src/hooks/useTaskStages.js @@ -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('Этап успешно удален'); })