From 0a97211a0cfd254f32b5f5efa8677df31797bc7d Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Tue, 18 Aug 2026 15:31:50 +0300 Subject: [PATCH] stage project --- web/src/api/stages.js | 18 +++ web/src/components/Stages/AddStagesModal.jsx | 19 ++- web/src/components/Stages/SettingModal.jsx | 136 +++++++++--------- .../Stages/StagesTable/StagesTable.jsx | 1 + web/src/components/common/CollapsibleTree.jsx | 1 + .../components/common/TableCard/TableCard.jsx | 4 +- .../components/common/TableList/TableList.jsx | 11 +- web/src/constants/constants.jsx | 2 + web/src/hooks/useProjectStages.js | 94 ++++++++++++ web/src/pages/TaskPage/TaskPage.jsx | 29 ++-- 10 files changed, 230 insertions(+), 85 deletions(-) create mode 100644 web/src/hooks/useProjectStages.js diff --git a/web/src/api/stages.js b/web/src/api/stages.js index 92963df..1049600 100644 --- a/web/src/api/stages.js +++ b/web/src/api/stages.js @@ -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( diff --git a/web/src/components/Stages/AddStagesModal.jsx b/web/src/components/Stages/AddStagesModal.jsx index 175364a..dcb1179 100644 --- a/web/src/components/Stages/AddStagesModal.jsx +++ b/web/src/components/Stages/AddStagesModal.jsx @@ -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 = ({ Выбор листа - Выберите лист @@ -207,6 +214,7 @@ export const AddStagesModal = ({ onChange={(e) => setSelectedDirection(e.target.value)} displayEmpty size='small' + MenuProps={stageSelectMenuProps} > Выберите направление @@ -242,7 +250,12 @@ export const AddStagesModal = ({ Роль - setSelectedRole(e.target.value)} + displayEmpty + size='small' + MenuProps={stageSelectMenuProps}> {Object.entries(STAGE_ROLE_RUSSIAN_NAME).map(([key, label]) => ( {label} @@ -292,4 +305,4 @@ export const AddStagesModal = ({ ); -}; \ No newline at end of file +}; diff --git a/web/src/components/Stages/SettingModal.jsx b/web/src/components/Stages/SettingModal.jsx index 8aa1cc7..4c45709 100644 --- a/web/src/components/Stages/SettingModal.jsx +++ b/web/src/components/Stages/SettingModal.jsx @@ -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 ( <> - - - - - - - + + + + }> + Добавить этап + + - - - Этапы - - }> - Добавить этап - - - - {stagesInfo.isLoading ? ( - - - - ) : ( - - )} - { - setIsAddEditModalOpen(false); - }} - stage={stageForEdit} - createStage={createStage} - updateStage={updateStage} - isTaskMode={isTaskMode} - isSaving={isSaving} - sheetOptions={sheetOptions} - formType={formTypeCode} - /> - {!isTaskMode && ( - { - setIsDeleteModalOpen(false); - setStageToDelete(null); - }} - onConfirm={handleConfirmDelete} - stage={stageToDelete} - /> - )} - - - - }> + {stagesInfo.isLoading ? ( + + + + ) : ( + + )} + { + setIsAddEditModalOpen(false); + }} + stage={stageForEdit} + createStage={createStage} + updateStage={updateStage} + isTaskMode={isTaskMode} + isSaving={isSaving} + sheetOptions={sheetOptions} + formType={formTypeCode} + defaultSheet={mode === 'project' ? reportType : ''} + /> + {!isTaskMode && ( + { + setIsDeleteModalOpen(false); + setStageToDelete(null); + }} + onConfirm={handleConfirmDelete} + stage={stageToDelete} + /> + )} + + ); }; diff --git a/web/src/components/Stages/StagesTable/StagesTable.jsx b/web/src/components/Stages/StagesTable/StagesTable.jsx index fdd7290..d42c6ac 100644 --- a/web/src/components/Stages/StagesTable/StagesTable.jsx +++ b/web/src/components/Stages/StagesTable/StagesTable.jsx @@ -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', diff --git a/web/src/components/common/CollapsibleTree.jsx b/web/src/components/common/CollapsibleTree.jsx index 2d785c2..2672bc4 100644 --- a/web/src/components/common/CollapsibleTree.jsx +++ b/web/src/components/common/CollapsibleTree.jsx @@ -198,6 +198,7 @@ const CollapsibleTree = ({ open={open} anchorEl={anchorEl} onClose={handleClose} + sx={forStage ? { zIndex: 70 } : undefined} anchorOrigin={{ vertical: 'bottom', horizontal: 'left', diff --git a/web/src/components/common/TableCard/TableCard.jsx b/web/src/components/common/TableCard/TableCard.jsx index ecb40ef..943fbcf 100644 --- a/web/src/components/common/TableCard/TableCard.jsx +++ b/web/src/components/common/TableCard/TableCard.jsx @@ -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 }) => { */} - +
event.stopPropagation()}>{action || }
); diff --git a/web/src/components/common/TableList/TableList.jsx b/web/src/components/common/TableList/TableList.jsx index a0bb1da..9cfde52 100644 --- a/web/src/components/common/TableList/TableList.jsx +++ b/web/src/components/common/TableList/TableList.jsx @@ -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 handleNavigate(path)} table={table} />; + return ( + handleNavigate(path)} + table={table} + action={renderTableAction?.(table)} + /> + ); }; const projectTablesByYear = isProject diff --git a/web/src/constants/constants.jsx b/web/src/constants/constants.jsx index ac2948e..c4ff25a 100644 --- a/web/src/constants/constants.jsx +++ b/web/src/constants/constants.jsx @@ -60,4 +60,6 @@ export const SHEET_NAME = { OTCH9F: 'Отчет 9ф', SMETA: 'Смета', STRUCTURE: 'Структура', + CURRENT_EXPENSES: 'Текущие расходы', + LIMIT: 'Лимиты', }; diff --git a/web/src/hooks/useProjectStages.js b/web/src/hooks/useProjectStages.js new file mode 100644 index 0000000..854f31f --- /dev/null +++ b/web/src/hooks/useProjectStages.js @@ -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 }; +}; diff --git a/web/src/pages/TaskPage/TaskPage.jsx b/web/src/pages/TaskPage/TaskPage.jsx index 5d08bba..4ee2a46 100644 --- a/web/src/pages/TaskPage/TaskPage.jsx +++ b/web/src/pages/TaskPage/TaskPage.jsx @@ -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() { - setModalStageOpen(true)}> - Этапы - + {!isProject && ( + setStageReport({})}> + Этапы + + )} {isProject && ( setAddYearModalOpen(true)}> Добавить год @@ -186,13 +188,15 @@ export default function TaskPage() { )} 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} /> @@ -204,6 +208,15 @@ export default function TaskPage() { basePath={'/table'} formInfo={task} isProject={isProject} + renderTableAction={ + isProject + ? (table) => ( + setStageReport(table)}> + Этапы + + ) + : undefined + } />