Merge pull request 'AURORA-1880: окно «Этапы» — этап с направлением в строке, правке и удалении (поверх #315)' (#317) from feat/316-aurora-1880-stages-direction-ui into test
This commit is contained in:
commit
30e4c4bf78
@ -1,28 +1,22 @@
|
|||||||
import api from "./client";
|
import api from "./client";
|
||||||
|
import { buildFormStageRequest } from "./stagesRequest";
|
||||||
|
|
||||||
export const StagesApi = {
|
export const StagesApi = {
|
||||||
createStage(formId, payload) {
|
createStage(formId, payload) {
|
||||||
return api.post(`/stages/form/${formId}`, payload).then((r) => r.data);
|
return api.post(`/stages/form/${formId}`, payload).then((r) => r.data);
|
||||||
},
|
},
|
||||||
updateStage(payload) {
|
updateStage(payload) {
|
||||||
const { budget_form_id, sheet, phase_code, direction } = payload;
|
const { path, params } = buildFormStageRequest(payload);
|
||||||
if (direction) {
|
return api.patch(path, payload, { params }).then((r) => r.data);
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
deleteStage(formId, sheet, phaseCode) {
|
deleteStage(formId, sheet, phaseCode, direction) {
|
||||||
return api
|
const { path, params } = buildFormStageRequest({
|
||||||
.delete(`/stages/form/${formId}/${sheet}/${phaseCode}`)
|
budget_form_id: formId,
|
||||||
.then((r) => r.data);
|
sheet,
|
||||||
|
phase_code: phaseCode,
|
||||||
|
direction,
|
||||||
|
});
|
||||||
|
return api.delete(path, { params }).then((r) => r.data);
|
||||||
},
|
},
|
||||||
getStages(formId) {
|
getStages(formId) {
|
||||||
return api.get(`/stages/form/${formId}`).then((r) => r.data);
|
return api.get(`/stages/form/${formId}`).then((r) => r.data);
|
||||||
|
|||||||
4
web/src/api/stagesRequest.js
Normal file
4
web/src/api/stagesRequest.js
Normal 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 } : {},
|
||||||
|
});
|
||||||
16
web/src/api/stagesRequest.test.js
Normal file
16
web/src/api/stagesRequest.test.js
Normal 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, {});
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
||||||
import { Button, Stack, Typography } from '@mui/material';
|
import { Button, Stack, Typography } from '@mui/material';
|
||||||
|
import { DIRECTION_TRANSLATE, SHEET_NAME } from '../../constants/constants';
|
||||||
import { ModalContainer as BaseModalContainer, ModalBackdrop } from '../common/Modal/FormModalStyled';
|
import { ModalContainer as BaseModalContainer, ModalBackdrop } from '../common/Modal/FormModalStyled';
|
||||||
|
|
||||||
const ModalContainer = styled(BaseModalContainer)`
|
const ModalContainer = styled(BaseModalContainer)`
|
||||||
@ -40,6 +41,13 @@ export const DeleteStageModal = ({ isOpen, onClose, onConfirm, stage }) => {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Typography sx={{ color: 'var(--GreyDark, rgba(74, 85, 101, 1))' }}>Удалить этот элемент?</Typography>
|
<Typography sx={{ color: 'var(--GreyDark, rgba(74, 85, 101, 1))' }}>Удалить этот элемент?</Typography>
|
||||||
|
|
||||||
|
{stage && (
|
||||||
|
<Typography sx={{ marginTop: '0.5rem', fontWeight: 500 }}>
|
||||||
|
{SHEET_NAME[stage.sheet] ?? stage.sheet}
|
||||||
|
{stage.direction ? ` (${DIRECTION_TRANSLATE[stage.direction] ?? stage.direction})` : ''} — {stage.phase_code}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
<Typography variant='grey' sx={{ marginTop: '0.5rem' }}>
|
<Typography variant='grey' sx={{ marginTop: '0.5rem' }}>
|
||||||
Он будет удалён без возможности восстановления.
|
Он будет удалён без возможности восстановления.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export const SettingModal = ({
|
|||||||
<StagesTable
|
<StagesTable
|
||||||
stages={stagesInfo.stages || []}
|
stages={stagesInfo.stages || []}
|
||||||
onEdit={handleEditStage}
|
onEdit={handleEditStage}
|
||||||
onDelete={!isTaskMode ? handleDeleteStage : undefined}
|
onDelete={handleDeleteStage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
@ -141,7 +141,7 @@ export const SettingModal = ({
|
|||||||
defaultSheet={mode === 'project' ? reportType : ''}
|
defaultSheet={mode === 'project' ? reportType : ''}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isTaskMode && (
|
{isAdmin && (
|
||||||
<DeleteStageModal
|
<DeleteStageModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|||||||
import { Divider, IconButton, Menu, MenuItem, TableBody, TableCell, TableRow, Typography } from '@mui/material';
|
import { Divider, IconButton, Menu, MenuItem, TableBody, TableCell, TableRow, Typography } from '@mui/material';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAuth } from '../../../app/context/AuthProvider';
|
import { useAuth } from '../../../app/context/AuthProvider';
|
||||||
import { ROLES_NAME_ID, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../constants/constants';
|
import { DIRECTION_TRANSLATE, ROLES_NAME_ID, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../constants/constants';
|
||||||
import { formatDate } from '../../../utils/formatDate';
|
import { formatDate } from '../../../utils/formatDate';
|
||||||
import { StageStatusChip } from '../StageStatusChip';
|
import { StageStatusChip } from '../StageStatusChip';
|
||||||
|
import { getStageKey } from '../utils';
|
||||||
import { StyledTable, StyledTableContainer, StyledTableHead, StyledTableHeadRow, StyledTableRow } from './StagesTable.style';
|
import { StyledTable, StyledTableContainer, StyledTableHead, StyledTableHeadRow, StyledTableRow } from './StagesTable.style';
|
||||||
import { getStageStatus, getStatusColors } from './utils';
|
import { getStageStatus, getStatusColors } from './utils';
|
||||||
|
|
||||||
@ -60,15 +61,15 @@ export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
stages.map((stage, index) => {
|
stages.map((stage) => {
|
||||||
const status = getStageStatus(stage);
|
const status = getStageStatus(stage);
|
||||||
const colors = getStatusColors(status);
|
const colors = getStatusColors(status);
|
||||||
const rowKey = `${stage.sheet}-${stage.phase_code}-${stage.role}-${index}`;
|
|
||||||
return (
|
return (
|
||||||
<StyledTableRow key={rowKey}>
|
<StyledTableRow key={getStageKey(stage)}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography variant='body2'>
|
<Typography variant='body2'>
|
||||||
{SHEET_NAME[stage.sheet]} - {stage.phase_code}
|
{SHEET_NAME[stage.sheet]}
|
||||||
|
{stage.direction ? ` (${DIRECTION_TRANSLATE[stage.direction] ?? stage.direction})` : ''} — {stage.phase_code}
|
||||||
</Typography>
|
</Typography>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Typography variant='caption' color='text.secondary' sx={{ fontSize: '0.65rem' }}>
|
<Typography variant='caption' color='text.secondary' sx={{ fontSize: '0.65rem' }}>
|
||||||
|
|||||||
@ -6,3 +6,11 @@ export const isPastDate = (value) => {
|
|||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
return date < today;
|
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('|');
|
||||||
|
|||||||
28
web/src/components/Stages/utils.test.js
Normal file
28
web/src/components/Stages/utils.test.js
Normal 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');
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { StagesApi } from '../api/stages';
|
import { StagesApi } from '../api/stages';
|
||||||
|
import { isSameStage } from '../components/Stages/utils';
|
||||||
|
|
||||||
export const useTaskStages = (taskId) => {
|
export const useTaskStages = (taskId) => {
|
||||||
const [stagesInfo, setStagesInfo] = useState({
|
const [stagesInfo, setStagesInfo] = useState({
|
||||||
@ -37,9 +38,7 @@ export const useTaskStages = (taskId) => {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
setStagesInfo((prev) => ({
|
setStagesInfo((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
stages: prev.stages.map((curStage) =>
|
stages: prev.stages.map((curStage) => (isSameStage(curStage, stage) ? data.result : curStage)),
|
||||||
curStage.sheet === stage.sheet && curStage.phase_code === stage.phase_code ? data.result : curStage,
|
|
||||||
),
|
|
||||||
}));
|
}));
|
||||||
toast.success('Данные об этапе обновлены');
|
toast.success('Данные об этапе обновлены');
|
||||||
return data;
|
return data;
|
||||||
@ -83,14 +82,14 @@ export const useTaskStages = (taskId) => {
|
|||||||
|
|
||||||
const deleteStage = useCallback(
|
const deleteStage = useCallback(
|
||||||
(stage) => {
|
(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;
|
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(() => {
|
.then(() => {
|
||||||
setStagesInfo((prev) => ({
|
setStagesInfo((prev) => ({
|
||||||
...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('Этап успешно удален');
|
toast.success('Этап успешно удален');
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user