Compare commits

..

No commits in common. "97ac7af85a9b5892ee069ddd0c62b2a0e0e4276b" and "086adc099228be06ba7cc249b38ce02f6e98edb3" have entirely different histories.

10 changed files with 85 additions and 230 deletions

View File

@ -27,24 +27,6 @@ export const StagesApi = {
getStages(formId) { getStages(formId) {
return api.get(`/stages/form/${formId}`).then((r) => r.data); 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) { createStageAccessHeader(tableId, stageId, payload) {
return api return api
.post( .post(

View File

@ -11,8 +11,6 @@ import { StageStatusChip } from './StageStatusChip';
import { getStageStatus, getStatusColors } from './StagesTable/utils'; import { getStageStatus, getStatusColors } from './StagesTable/utils';
import { stageColumnsHiddenFromPicker } from './constants'; import { stageColumnsHiddenFromPicker } from './constants';
const stageSelectMenuProps = { sx: { zIndex: 70 } };
export const AddStagesModal = ({ export const AddStagesModal = ({
isOpen, isOpen,
onClose, onClose,
@ -186,12 +184,7 @@ export const AddStagesModal = ({
<Stack sx={{ marginBottom: '0.75rem', width: '100%' }}> <Stack sx={{ marginBottom: '0.75rem', width: '100%' }}>
<FormControl fullWidth> <FormControl fullWidth>
<FormLabel>Выбор листа</FormLabel> <FormLabel>Выбор листа</FormLabel>
<Select <Select value={selectedSheet} onChange={handleSheetChange} displayEmpty size='small'>
value={selectedSheet}
onChange={handleSheetChange}
displayEmpty
size='small'
MenuProps={stageSelectMenuProps}>
<MenuItem value='' disabled> <MenuItem value='' disabled>
Выберите лист Выберите лист
</MenuItem> </MenuItem>
@ -214,7 +207,6 @@ export const AddStagesModal = ({
onChange={(e) => setSelectedDirection(e.target.value)} onChange={(e) => setSelectedDirection(e.target.value)}
displayEmpty displayEmpty
size='small' size='small'
MenuProps={stageSelectMenuProps}
> >
<MenuItem value='' disabled> <MenuItem value='' disabled>
Выберите направление Выберите направление
@ -250,12 +242,7 @@ export const AddStagesModal = ({
<Stack sx={{ marginBottom: '0.75rem', width: '100%' }}> <Stack sx={{ marginBottom: '0.75rem', width: '100%' }}>
<FormControl fullWidth> <FormControl fullWidth>
<FormLabel>Роль</FormLabel> <FormLabel>Роль</FormLabel>
<Select <Select value={selectedRole ?? ''} onChange={(e) => setSelectedRole(e.target.value)} displayEmpty size='small'>
value={selectedRole ?? ''}
onChange={(e) => setSelectedRole(e.target.value)}
displayEmpty
size='small'
MenuProps={stageSelectMenuProps}>
{Object.entries(STAGE_ROLE_RUSSIAN_NAME).map(([key, label]) => ( {Object.entries(STAGE_ROLE_RUSSIAN_NAME).map(([key, label]) => (
<MenuItem key={key} value={key}> <MenuItem key={key} value={key}>
{label} {label}

View File

@ -1,11 +1,10 @@
import { Box, CircularProgress, Stack } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close';
import { Box, CircularProgress, IconButton, Stack, Typography } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTaskStages } from '../../hooks/useTaskStages'; import { useTaskStages } from '../../hooks/useTaskStages';
import { useProjectStages } from '../../hooks/useProjectStages';
import { SHEET_NAME } from '../../constants/constants';
import { PrimaryButton } from '../common/Buttons/Buttons'; import { PrimaryButton } from '../common/Buttons/Buttons';
import Modal from '../common/Modal/Modal'; import { ModalSetting } from '../common/Modal/ModalSetting';
import { EditContainer } from '../common/Modal/ModalStyled'; import { EditContainer, ModalContainer, ModalContent } from '../common/Modal/ModalStyled';
import { WhitePlus } from '../common/icons/icons'; import { WhitePlus } from '../common/icons/icons';
import { AddStagesModal } from './AddStagesModal'; import { AddStagesModal } from './AddStagesModal';
import { DeleteStageModal } from './DeleteStageModal'; import { DeleteStageModal } from './DeleteStageModal';
@ -16,25 +15,16 @@ export const SettingModal = ({
onClose, onClose,
formId, formId,
taskId, taskId,
projectId,
year,
reportType,
mode = 'form', // 'form' | 'task' mode = 'form', // 'form' | 'task'
formTypeCode = '', formTypeCode = '',
sheets, sheets,
}) => { }) => {
const taskStages = useTaskStages(taskId); const taskStages = useTaskStages(taskId);
const projectStages = useProjectStages(projectId, year, reportType);
const stages = mode === 'project' ? projectStages : taskStages;
const { deleteStage, stagesInfo, getStages, createStage, updateStage, isSaving } = stages; const { deleteStage, stagesInfo, getStages, createStage, updateStage, isSaving } = taskStages;
const isTaskMode = mode === 'task'; const isTaskMode = mode === 'task';
const entityId = mode === 'project' ? projectId : taskId; const entityId = taskId;
const projectSheetTitle = SHEET_NAME[reportType] || reportType;
const modalTitle = mode === 'project' && reportType
? `Этапы — ${projectSheetTitle}${year ? `, ${year} год` : ''}`
: 'Этапы';
useEffect(() => { useEffect(() => {
if (isOpen && entityId) { if (isOpen && entityId) {
@ -135,9 +125,22 @@ export const SettingModal = ({
return ( return (
<> <>
<Modal open={isOpen} onClose={onClose} title={modalTitle} customSize={50}> <ModalSetting
<EditContainer style={{ margin: 0 }}> isOpen={isOpen}
onClose={onClose}
children={
<ModalContainer>
<ModalContent>
<Stack direction='row' sx={{ width: '100%', justifyContent: 'flex-end' }}> <Stack direction='row' sx={{ width: '100%', justifyContent: 'flex-end' }}>
<IconButton size='small' onClick={onClose}>
<CloseIcon />
</IconButton>
</Stack>
<EditContainer>
<Stack direction='row' sx={{ width: '100%', justifyContent: 'space-between' }}>
<Typography sx={{ fontWeight: 'bold' }}>Этапы</Typography>
<PrimaryButton onClick={handleAddStage} startIcon={<WhitePlus />}> <PrimaryButton onClick={handleAddStage} startIcon={<WhitePlus />}>
Добавить этап Добавить этап
</PrimaryButton> </PrimaryButton>
@ -171,7 +174,6 @@ export const SettingModal = ({
isSaving={isSaving} isSaving={isSaving}
sheetOptions={sheetOptions} sheetOptions={sheetOptions}
formType={formTypeCode} formType={formTypeCode}
defaultSheet={mode === 'project' ? reportType : ''}
/> />
{!isTaskMode && ( {!isTaskMode && (
<DeleteStageModal <DeleteStageModal
@ -185,7 +187,9 @@ export const SettingModal = ({
/> />
)} )}
</EditContainer> </EditContainer>
</Modal> </ModalContent>
</ModalContainer>
}></ModalSetting>
</> </>
); );
}; };

View File

@ -103,7 +103,6 @@ export const StagesTable = ({ stages, onEdit, onDelete }) => {
anchorEl={anchorEl} anchorEl={anchorEl}
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
onClose={handleCloseMenu} onClose={handleCloseMenu}
sx={{ zIndex: 60 }}
anchorOrigin={{ anchorOrigin={{
vertical: 'bottom', vertical: 'bottom',
horizontal: 'right', horizontal: 'right',

View File

@ -198,7 +198,6 @@ const CollapsibleTree = ({
open={open} open={open}
anchorEl={anchorEl} anchorEl={anchorEl}
onClose={handleClose} onClose={handleClose}
sx={forStage ? { zIndex: 70 } : undefined}
anchorOrigin={{ anchorOrigin={{
vertical: 'bottom', vertical: 'bottom',
horizontal: 'left', horizontal: 'left',

View File

@ -1,7 +1,7 @@
import { DIRECTION_TRANSLATE } from '../../../constants/constants'; import { DIRECTION_TRANSLATE } from '../../../constants/constants';
import { Right, Section, SectionStartPart, SectionTitle } from './TableCard.styled'; import { Right, Section, SectionStartPart, SectionTitle } from './TableCard.styled';
const TableCard = ({ table, onNavigate, action }) => { const TableCard = ({ table, onNavigate }) => {
const handleClick = () => { const handleClick = () => {
if (onNavigate) { if (onNavigate) {
onNavigate(table.id); onNavigate(table.id);
@ -23,7 +23,7 @@ const TableCard = ({ table, onNavigate, action }) => {
</UsersList> */} </UsersList> */}
</SectionTitle> </SectionTitle>
</SectionStartPart> </SectionStartPart>
<div onClick={(event) => event.stopPropagation()}>{action || <Right />}</div> <Right />
</Section> </Section>
</div> </div>
); );

View File

@ -2,7 +2,7 @@ import { Box, Typography } from '@mui/material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import TableCard from '../TableCard/TableCard'; import TableCard from '../TableCard/TableCard';
const TablesList = ({ filteredTables, query, basePath = '/table', formInfo, isProject = false, renderTableAction }) => { const TablesList = ({ filteredTables, query, basePath = '/table', formInfo, isProject = false }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleNavigate = (path) => { const handleNavigate = (path) => {
@ -16,14 +16,7 @@ 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}`; const path = `${basePath}/form/${formInfo.id}/form-type/${isProject ? 'PROJECT' : formInfo.form_type_code}/${table.sheet}/${table?.direction}/${table?.year}`;
return ( return <TableCard key={`${table.sheet}_${table?.direction}_${table?.year}`} onNavigate={() => handleNavigate(path)} table={table} />;
<TableCard
key={`${table.sheet}_${table?.direction}_${table?.year}`}
onNavigate={() => handleNavigate(path)}
table={table}
action={renderTableAction?.(table)}
/>
);
}; };
const projectTablesByYear = isProject const projectTablesByYear = isProject

View File

@ -60,6 +60,4 @@ export const SHEET_NAME = {
OTCH9F: 'Отчет 9ф', OTCH9F: 'Отчет 9ф',
SMETA: 'Смета', SMETA: 'Смета',
STRUCTURE: 'Структура', STRUCTURE: 'Структура',
CURRENT_EXPENSES: 'Текущие расходы',
LIMIT: 'Лимиты',
}; };

View File

@ -1,94 +0,0 @@
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 };
};

View File

@ -46,7 +46,7 @@ export default function TaskPage() {
const [tables, setTables] = useState([]); const [tables, setTables] = useState([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [stageReport, setStageReport] = useState(null); const [modalStageOpen, setModalStageOpen] = useState(false);
const [modalProjectOpen, setModalProjectOpen] = useState(false); const [modalProjectOpen, setModalProjectOpen] = useState(false);
const [tempProjects, setTempProjects] = useState([]); const [tempProjects, setTempProjects] = useState([]);
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
@ -176,11 +176,9 @@ export default function TaskPage() {
<SearchComponent placeholder='Поиск по названию или направлению' value={query} onChange={setQuery} /> <SearchComponent placeholder='Поиск по названию или направлению' value={query} onChange={setQuery} />
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end', gap: '.5rem' }}> <Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end', gap: '.5rem' }}>
<ExportWithTextButton onClick={handleExport} /> <ExportWithTextButton onClick={handleExport} />
{!isProject && ( <PrimaryOutlinedButton variant='outlined' onClick={() => setModalStageOpen(true)}>
<PrimaryOutlinedButton variant='outlined' onClick={() => setStageReport({})}>
<span>Этапы</span> <span>Этапы</span>
</PrimaryOutlinedButton> </PrimaryOutlinedButton>
)}
{isProject && ( {isProject && (
<PrimaryButton onClick={() => setAddYearModalOpen(true)}> <PrimaryButton onClick={() => setAddYearModalOpen(true)}>
Добавить год Добавить год
@ -188,15 +186,13 @@ export default function TaskPage() {
)} )}
<SettingStageModal <SettingStageModal
isOpen={stageReport !== null} isOpen={modalStageOpen}
onClose={() => setStageReport(null)} onClose={() => setModalStageOpen(false)}
taskId={isProject ? undefined : taskId} taskId={isProject ? undefined : taskId}
projectId={isProject ? projectId : undefined} projectId={isProject ? projectId : undefined}
year={stageReport?.year}
reportType={stageReport?.sheet}
mode={isProject ? 'project' : 'task'} mode={isProject ? 'project' : 'task'}
formTypeCode={isProject ? 'PROJECT' : task?.form_type_code || ''} formTypeCode={task?.form_type_code || ''}
sheets={isProject && stageReport ? [stageReport] : tables} sheets={tables}
/> />
</Stack> </Stack>
</Box> </Box>
@ -208,15 +204,6 @@ export default function TaskPage() {
basePath={'/table'} basePath={'/table'}
formInfo={task} formInfo={task}
isProject={isProject} isProject={isProject}
renderTableAction={
isProject
? (table) => (
<PrimaryOutlinedButton variant='outlined' onClick={() => setStageReport(table)}>
Этапы
</PrimaryOutlinedButton>
)
: undefined
}
/> />
</Box> </Box>
</Box> </Box>