stage project #102
@ -27,6 +27,24 @@ export const StagesApi = {
|
||||
getStages(formId) {
|
||||
return api.get(`/stages/form/${formId}`).then((r) => r.data);
|
||||
},
|
||||
getProjectStages(projectId, year, reportType) {
|
||||
return api
|
||||
.get(`/stages/project/${projectId}`, { params: { year, report_type: reportType } })
|
||||
.then((r) => r.data);
|
||||
},
|
||||
createProjectStage(projectId, payload) {
|
||||
return api.post(`/stages/project/${projectId}`, payload).then((r) => r.data);
|
||||
},
|
||||
updateProjectStage(projectId, year, reportType, phaseCode, payload) {
|
||||
return api
|
||||
.patch(`/stages/project/${projectId}/${year}/${reportType}/${encodeURIComponent(phaseCode)}`, payload)
|
||||
.then((r) => r.data);
|
||||
},
|
||||
deleteProjectStage(projectId, year, reportType, phaseCode) {
|
||||
return api
|
||||
.delete(`/stages/project/${projectId}/${year}/${reportType}/${encodeURIComponent(phaseCode)}`)
|
||||
.then((r) => r.data);
|
||||
},
|
||||
createStageAccessHeader(tableId, stageId, payload) {
|
||||
return api
|
||||
.post(
|
||||
|
||||
@ -11,6 +11,8 @@ import { StageStatusChip } from './StageStatusChip';
|
||||
import { getStageStatus, getStatusColors } from './StagesTable/utils';
|
||||
import { stageColumnsHiddenFromPicker } from './constants';
|
||||
|
||||
const stageSelectMenuProps = { sx: { zIndex: 70 } };
|
||||
|
||||
export const AddStagesModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@ -184,7 +186,12 @@ export const AddStagesModal = ({
|
||||
<Stack sx={{ marginBottom: '0.75rem', width: '100%' }}>
|
||||
<FormControl fullWidth>
|
||||
<FormLabel>Выбор листа</FormLabel>
|
||||
<Select value={selectedSheet} onChange={handleSheetChange} displayEmpty size='small'>
|
||||
<Select
|
||||
value={selectedSheet}
|
||||
onChange={handleSheetChange}
|
||||
displayEmpty
|
||||
size='small'
|
||||
MenuProps={stageSelectMenuProps}>
|
||||
<MenuItem value='' disabled>
|
||||
Выберите лист
|
||||
</MenuItem>
|
||||
@ -207,6 +214,7 @@ export const AddStagesModal = ({
|
||||
onChange={(e) => setSelectedDirection(e.target.value)}
|
||||
displayEmpty
|
||||
size='small'
|
||||
MenuProps={stageSelectMenuProps}
|
||||
>
|
||||
<MenuItem value='' disabled>
|
||||
Выберите направление
|
||||
@ -242,7 +250,12 @@ export const AddStagesModal = ({
|
||||
<Stack sx={{ marginBottom: '0.75rem', width: '100%' }}>
|
||||
<FormControl fullWidth>
|
||||
<FormLabel>Роль</FormLabel>
|
||||
<Select value={selectedRole ?? ''} onChange={(e) => setSelectedRole(e.target.value)} displayEmpty size='small'>
|
||||
<Select
|
||||
value={selectedRole ?? ''}
|
||||
onChange={(e) => setSelectedRole(e.target.value)}
|
||||
displayEmpty
|
||||
size='small'
|
||||
MenuProps={stageSelectMenuProps}>
|
||||
{Object.entries(STAGE_ROLE_RUSSIAN_NAME).map(([key, label]) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{label}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { Box, CircularProgress, IconButton, Stack, Typography } from '@mui/material';
|
||||
import { Box, CircularProgress, Stack } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTaskStages } from '../../hooks/useTaskStages';
|
||||
import { useProjectStages } from '../../hooks/useProjectStages';
|
||||
import { SHEET_NAME } from '../../constants/constants';
|
||||
import { PrimaryButton } from '../common/Buttons/Buttons';
|
||||
import { ModalSetting } from '../common/Modal/ModalSetting';
|
||||
import { EditContainer, ModalContainer, ModalContent } from '../common/Modal/ModalStyled';
|
||||
import Modal from '../common/Modal/Modal';
|
||||
import { EditContainer } from '../common/Modal/ModalStyled';
|
||||
import { WhitePlus } from '../common/icons/icons';
|
||||
import { AddStagesModal } from './AddStagesModal';
|
||||
import { DeleteStageModal } from './DeleteStageModal';
|
||||
@ -15,16 +16,25 @@ export const SettingModal = ({
|
||||
onClose,
|
||||
formId,
|
||||
taskId,
|
||||
projectId,
|
||||
year,
|
||||
reportType,
|
||||
mode = 'form', // 'form' | 'task'
|
||||
formTypeCode = '',
|
||||
sheets,
|
||||
}) => {
|
||||
const taskStages = useTaskStages(taskId);
|
||||
const projectStages = useProjectStages(projectId, year, reportType);
|
||||
const stages = mode === 'project' ? projectStages : taskStages;
|
||||
|
||||
const { deleteStage, stagesInfo, getStages, createStage, updateStage, isSaving } = taskStages;
|
||||
const { deleteStage, stagesInfo, getStages, createStage, updateStage, isSaving } = stages;
|
||||
|
||||
const isTaskMode = mode === 'task';
|
||||
const entityId = taskId;
|
||||
const entityId = mode === 'project' ? projectId : taskId;
|
||||
const projectSheetTitle = SHEET_NAME[reportType] || reportType;
|
||||
const modalTitle = mode === 'project' && reportType
|
||||
? `Этапы — ${projectSheetTitle}${year ? `, ${year} год` : ''}`
|
||||
: 'Этапы';
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && entityId) {
|
||||
@ -125,71 +135,57 @@ export const SettingModal = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalSetting
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
children={
|
||||
<ModalContainer>
|
||||
<ModalContent>
|
||||
<Stack direction='row' sx={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<IconButton size='small' onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Modal open={isOpen} onClose={onClose} title={modalTitle} customSize={50}>
|
||||
<EditContainer style={{ margin: 0 }}>
|
||||
<Stack direction='row' sx={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<PrimaryButton onClick={handleAddStage} startIcon={<WhitePlus />}>
|
||||
Добавить этап
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
|
||||
<EditContainer>
|
||||
<Stack direction='row' sx={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Typography sx={{ fontWeight: 'bold' }}>Этапы</Typography>
|
||||
|
||||
<PrimaryButton onClick={handleAddStage} startIcon={<WhitePlus />}>
|
||||
Добавить этап
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
|
||||
{stagesInfo.isLoading ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 4,
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<StagesTable
|
||||
stages={stagesInfo.stages || []}
|
||||
onEdit={handleEditStage}
|
||||
onDelete={!isTaskMode ? handleDeleteStage : undefined}
|
||||
/>
|
||||
)}
|
||||
<AddStagesModal
|
||||
isOpen={isAddEditModalOpen}
|
||||
onClose={() => {
|
||||
setIsAddEditModalOpen(false);
|
||||
}}
|
||||
stage={stageForEdit}
|
||||
createStage={createStage}
|
||||
updateStage={updateStage}
|
||||
isTaskMode={isTaskMode}
|
||||
isSaving={isSaving}
|
||||
sheetOptions={sheetOptions}
|
||||
formType={formTypeCode}
|
||||
/>
|
||||
{!isTaskMode && (
|
||||
<DeleteStageModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setStageToDelete(null);
|
||||
}}
|
||||
onConfirm={handleConfirmDelete}
|
||||
stage={stageToDelete}
|
||||
/>
|
||||
)}
|
||||
</EditContainer>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
}></ModalSetting>
|
||||
{stagesInfo.isLoading ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 4,
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<StagesTable
|
||||
stages={stagesInfo.stages || []}
|
||||
onEdit={handleEditStage}
|
||||
onDelete={!isTaskMode ? handleDeleteStage : undefined}
|
||||
/>
|
||||
)}
|
||||
<AddStagesModal
|
||||
isOpen={isAddEditModalOpen}
|
||||
onClose={() => {
|
||||
setIsAddEditModalOpen(false);
|
||||
}}
|
||||
stage={stageForEdit}
|
||||
createStage={createStage}
|
||||
updateStage={updateStage}
|
||||
isTaskMode={isTaskMode}
|
||||
isSaving={isSaving}
|
||||
sheetOptions={sheetOptions}
|
||||
formType={formTypeCode}
|
||||
defaultSheet={mode === 'project' ? reportType : ''}
|
||||
/>
|
||||
{!isTaskMode && (
|
||||
<DeleteStageModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setStageToDelete(null);
|
||||
}}
|
||||
onConfirm={handleConfirmDelete}
|
||||
stage={stageToDelete}
|
||||
/>
|
||||
)}
|
||||
</EditContainer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -103,6 +103,7 @@ export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleCloseMenu}
|
||||
sx={{ zIndex: 60 }}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
|
||||
@ -198,6 +198,7 @@ const CollapsibleTree = ({
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
sx={forStage ? { zIndex: 70 } : undefined}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { DIRECTION_TRANSLATE } from '../../../constants/constants';
|
||||
import { Right, Section, SectionStartPart, SectionTitle } from './TableCard.styled';
|
||||
|
||||
const TableCard = ({ table, onNavigate }) => {
|
||||
const TableCard = ({ table, onNavigate, action }) => {
|
||||
const handleClick = () => {
|
||||
if (onNavigate) {
|
||||
onNavigate(table.id);
|
||||
@ -23,7 +23,7 @@ const TableCard = ({ table, onNavigate }) => {
|
||||
</UsersList> */}
|
||||
</SectionTitle>
|
||||
</SectionStartPart>
|
||||
<Right />
|
||||
<div onClick={(event) => event.stopPropagation()}>{action || <Right />}</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,7 +2,7 @@ import { Box, Typography } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import TableCard from '../TableCard/TableCard';
|
||||
|
||||
const TablesList = ({ filteredTables, query, basePath = '/table', formInfo, isProject = false }) => {
|
||||
const TablesList = ({ filteredTables, query, basePath = '/table', formInfo, isProject = false, renderTableAction }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleNavigate = (path) => {
|
||||
@ -16,7 +16,14 @@ const TablesList = ({ filteredTables, query, basePath = '/table', formInfo, isPr
|
||||
}
|
||||
const path = `${basePath}/form/${formInfo.id}/form-type/${isProject ? 'PROJECT' : formInfo.form_type_code}/${table.sheet}/${table?.direction}/${table?.year}`;
|
||||
|
||||
return <TableCard key={`${table.sheet}_${table?.direction}_${table?.year}`} onNavigate={() => handleNavigate(path)} table={table} />;
|
||||
return (
|
||||
<TableCard
|
||||
key={`${table.sheet}_${table?.direction}_${table?.year}`}
|
||||
onNavigate={() => handleNavigate(path)}
|
||||
table={table}
|
||||
action={renderTableAction?.(table)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const projectTablesByYear = isProject
|
||||
|
||||
@ -60,4 +60,6 @@ export const SHEET_NAME = {
|
||||
OTCH9F: 'Отчет 9ф',
|
||||
SMETA: 'Смета',
|
||||
STRUCTURE: 'Структура',
|
||||
CURRENT_EXPENSES: 'Текущие расходы',
|
||||
LIMIT: 'Лимиты',
|
||||
};
|
||||
|
||||
94
web/src/hooks/useProjectStages.js
Normal file
94
web/src/hooks/useProjectStages.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { StagesApi } from '../api/stages';
|
||||
|
||||
export const useProjectStages = (projectId, year, reportType) => {
|
||||
const [stagesInfo, setStagesInfo] = useState({ isLoading: false, stages: [] });
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const normalizeStage = useCallback(
|
||||
(stage) => ({ ...stage, sheet: stage.report_type || reportType, year: stage.year || year }),
|
||||
[reportType, year],
|
||||
);
|
||||
|
||||
const getStages = useCallback(() => {
|
||||
if (!projectId || !year || !reportType) return;
|
||||
setStagesInfo((prev) => ({ ...prev, isLoading: true }));
|
||||
|
||||
StagesApi.getProjectStages(projectId, year, reportType)
|
||||
.then((data) => {
|
||||
setStagesInfo({ stages: (data.result ?? []).map(normalizeStage), isLoading: false });
|
||||
})
|
||||
.catch((error) => {
|
||||
setStagesInfo((prev) => ({ ...prev, isLoading: false }));
|
||||
toast.error(error?.response?.data?.detail || 'Ошибка при получении этапов');
|
||||
});
|
||||
}, [normalizeStage, projectId, reportType, year]);
|
||||
|
||||
const createStage = useCallback(
|
||||
(payload) => {
|
||||
if (!projectId || !year || !reportType) return Promise.reject('Не задан контекст отчёта проекта');
|
||||
const { sheet, direction, ...stageData } = payload;
|
||||
setIsSaving(true);
|
||||
return StagesApi.createProjectStage(projectId, { ...stageData, year, report_type: reportType })
|
||||
.then((data) => {
|
||||
setStagesInfo((prev) => ({ ...prev, stages: [...prev.stages, normalizeStage(data.result)] }));
|
||||
toast.success('Этап добавлен');
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.response?.data?.detail || 'Ошибка добавления этапа');
|
||||
throw error;
|
||||
})
|
||||
.finally(() => setIsSaving(false));
|
||||
},
|
||||
[normalizeStage, projectId, reportType, year],
|
||||
);
|
||||
|
||||
const updateStage = useCallback(
|
||||
(stage) => {
|
||||
if (!projectId || !year || !reportType) return Promise.reject('Не задан контекст отчёта проекта');
|
||||
const { phase_code, role, column_keys, opens_at, closes_at } = stage;
|
||||
setIsSaving(true);
|
||||
return StagesApi.updateProjectStage(projectId, year, reportType, phase_code, {
|
||||
role,
|
||||
column_keys,
|
||||
opens_at,
|
||||
closes_at,
|
||||
})
|
||||
.then((data) => {
|
||||
const updatedStage = normalizeStage(data.result);
|
||||
setStagesInfo((prev) => ({
|
||||
...prev,
|
||||
stages: prev.stages.map((item) => (item.phase_code === phase_code ? updatedStage : item)),
|
||||
}));
|
||||
toast.success('Данные об этапе обновлены');
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.response?.data?.detail || 'Ошибка при обновлении этапа');
|
||||
throw error;
|
||||
})
|
||||
.finally(() => setIsSaving(false));
|
||||
},
|
||||
[normalizeStage, projectId, reportType, year],
|
||||
);
|
||||
|
||||
const deleteStage = useCallback(
|
||||
(stage) => {
|
||||
if (!projectId || !year || !reportType || !stage?.phase_code) return;
|
||||
StagesApi.deleteProjectStage(projectId, year, reportType, stage.phase_code)
|
||||
.then(() => {
|
||||
setStagesInfo((prev) => ({
|
||||
...prev,
|
||||
stages: prev.stages.filter((item) => item.phase_code !== stage.phase_code),
|
||||
}));
|
||||
toast.success('Этап успешно удален');
|
||||
})
|
||||
.catch((error) => toast.error(error?.response?.data?.detail || 'Ошибка при удалении этапа'));
|
||||
},
|
||||
[projectId, reportType, year],
|
||||
);
|
||||
|
||||
return { stagesInfo, getStages, createStage, updateStage, deleteStage, isSaving };
|
||||
};
|
||||
@ -46,7 +46,7 @@ export default function TaskPage() {
|
||||
const [tables, setTables] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const [modalStageOpen, setModalStageOpen] = useState(false);
|
||||
const [stageReport, setStageReport] = useState(null);
|
||||
const [modalProjectOpen, setModalProjectOpen] = useState(false);
|
||||
const [tempProjects, setTempProjects] = useState([]);
|
||||
const [projects, setProjects] = useState([]);
|
||||
@ -176,9 +176,11 @@ export default function TaskPage() {
|
||||
<SearchComponent placeholder='Поиск по названию или направлению' value={query} onChange={setQuery} />
|
||||
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end', gap: '.5rem' }}>
|
||||
<ExportWithTextButton onClick={handleExport} />
|
||||
<PrimaryOutlinedButton variant='outlined' onClick={() => setModalStageOpen(true)}>
|
||||
<span>Этапы</span>
|
||||
</PrimaryOutlinedButton>
|
||||
{!isProject && (
|
||||
<PrimaryOutlinedButton variant='outlined' onClick={() => setStageReport({})}>
|
||||
<span>Этапы</span>
|
||||
</PrimaryOutlinedButton>
|
||||
)}
|
||||
{isProject && (
|
||||
<PrimaryButton onClick={() => setAddYearModalOpen(true)}>
|
||||
Добавить год
|
||||
@ -186,13 +188,15 @@ export default function TaskPage() {
|
||||
)}
|
||||
|
||||
<SettingStageModal
|
||||
isOpen={modalStageOpen}
|
||||
onClose={() => setModalStageOpen(false)}
|
||||
isOpen={stageReport !== null}
|
||||
onClose={() => setStageReport(null)}
|
||||
taskId={isProject ? undefined : taskId}
|
||||
projectId={isProject ? projectId : undefined}
|
||||
year={stageReport?.year}
|
||||
reportType={stageReport?.sheet}
|
||||
mode={isProject ? 'project' : 'task'}
|
||||
formTypeCode={task?.form_type_code || ''}
|
||||
sheets={tables}
|
||||
formTypeCode={isProject ? 'PROJECT' : task?.form_type_code || ''}
|
||||
sheets={isProject && stageReport ? [stageReport] : tables}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
@ -204,6 +208,15 @@ export default function TaskPage() {
|
||||
basePath={'/table'}
|
||||
formInfo={task}
|
||||
isProject={isProject}
|
||||
renderTableAction={
|
||||
isProject
|
||||
? (table) => (
|
||||
<PrimaryOutlinedButton variant='outlined' onClick={() => setStageReport(table)}>
|
||||
Этапы
|
||||
</PrimaryOutlinedButton>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user