fix-stages: поправлены ручки по этапам и отображение данных

This commit is contained in:
Veronika Tsygankova 2026-06-05 16:57:41 +03:00
parent ee6fafb84f
commit 3970fe5c35
19 changed files with 226 additions and 268 deletions

View File

@ -2,20 +2,25 @@ import api from './client';
export const StagesApi = { export const StagesApi = {
createStage(formId, payload) { createStage(formId, payload) {
return api.put(`/forms/${formId}/stages`, [payload]).then((r) => r.data); return api.post(`/stages/form/${formId}`, [payload]).then((r) => r.data);
}, },
updateStage(formId, stage) { updateStage(payload) {
const { budget_form_id, sheet, phase_code, opens_at, closes_at } = payload;
return api return api
.patch(`/forms/${formId}/stages/${stage.id}`, stage) .patch(`/stages/form/${budget_form_id}/${sheet}/${phase_code}`, {
opens_at,
closes_at,
})
.then((r) => r.data); .then((r) => r.data);
}, },
deleteStage(formId, stage) { deleteStage(formId, sheet, phaseCode) {
return api return api
.delete(`/forms/${formId}/stages/${stage.id}`) .delete(`/stages/form/${formId}/${sheet}/${phaseCode}`)
.then((r) => r.data); .then((r) => r.data);
}, },
getStages(formId) { getStages(formId) {
return api.get(`/forms/${formId}/stages`).then((r) => r.data); return api.get(`/stages/form/${formId}`).then((r) => r.data);
}, },
createStageAccessHeader(tableId, stageId, payload) { createStageAccessHeader(tableId, stageId, payload) {
return api return api

View File

@ -13,9 +13,6 @@ export const TasksApi = {
getById(formId) { getById(formId) {
return api.get(`/form/${formId}`).then((r) => r.data); return api.get(`/form/${formId}`).then((r) => r.data);
}, },
getTaskStages(taskId) {
return api.get(`/tasks/${taskId}/stages`).then((r) => r.data);
},
create(payload) { create(payload) {
return api.put("/tasks/", payload).then((r) => r.data); return api.put("/tasks/", payload).then((r) => r.data);
}, },

View File

@ -30,42 +30,40 @@ export const AddStagesModal = ({
isTaskMode = false, isTaskMode = false,
isSaving = false, isSaving = false,
}) => { }) => {
const [stageName, setStageName] = useState('');
const [startDate, setStartDate] = useState(null); const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null); const [endDate, setEndDate] = useState(null);
useEffect(() => { useEffect(() => {
if (stage !== null) { if (stage !== null) {
setStageName(stage.name); setStartDate(stage.opens_at ?? null);
setStartDate(stage.period_start || null); setEndDate(stage.closes_at ?? null);
setEndDate(stage.period_end || null);
} else { } else {
setStageName('');
setStartDate(null); setStartDate(null);
setEndDate(null); setEndDate(null);
} }
}, [stage, isOpen]); }, [stage, isOpen]);
const handleSave = () => { const handleSave = () => {
const periodStart = formatDateForApi(startDate); const opensAt = formatDateForApi(startDate);
const periodEnd = formatDateForApi(endDate); const closesAt = formatDateForApi(endDate);
if (stage === null) { if (stage === null) {
createStage(stageName, periodStart, periodEnd)?.then(() => { if (!createStage) return;
createStage(opensAt, closesAt)?.then(() => {
handleCancel(); handleCancel();
}); });
} else { } else {
stage.name = stageName; updateStage({
stage.period_start = periodStart; ...stage,
stage.period_end = periodEnd; opens_at: opensAt,
updateStage(stage)?.then(() => { closes_at: closesAt,
})?.then(() => {
handleCancel(); handleCancel();
}); });
} }
}; };
const handleCancel = () => { const handleCancel = () => {
setStageName('');
setStartDate(null); setStartDate(null);
setEndDate(null); setEndDate(null);
onClose(); onClose();
@ -77,7 +75,7 @@ export const AddStagesModal = ({
} }
}; };
const isFormValid = stageName && startDate && endDate; const isFormValid = startDate && endDate;
if (!isOpen) return null; if (!isOpen) return null;
@ -88,7 +86,7 @@ export const AddStagesModal = ({
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}> <ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
<ModalContainer> <ModalContainer>
<HeaderContainer> <HeaderContainer>
<Stack direction="row" spacing={2} alignItems="center"> <Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<ModalTitle> <ModalTitle>
{stage ? 'Редактирование этапа' : 'Добавление этапа'} {stage ? 'Редактирование этапа' : 'Добавление этапа'}
</ModalTitle> </ModalTitle>
@ -108,22 +106,6 @@ export const AddStagesModal = ({
<Box> <Box>
<Divider sx={{ marginBottom: '0.75rem' }} /> <Divider sx={{ marginBottom: '0.75rem' }} />
<Stack
direction="column"
spacing={0.5}
sx={{ marginBottom: '0.75rem' }}
>
<FormControl fullWidth>
<FormLabel>Название этапа</FormLabel>
<TextField
value={stageName}
onChange={(e) => setStageName(e.target.value)}
placeholder="Введите название этапа"
fullWidth
size="small"
/>
</FormControl>
</Stack>
<Stack <Stack
direction="row" direction="row"

View File

@ -6,97 +6,74 @@ import {
CalendarDay, CalendarDay,
CalendarWeekday, CalendarWeekday,
} from './Calendar.styles'; } from './Calendar.styles';
import { YearTabs } from './YearTabs';
import { CalendarHeader } from './CalendarHeader'; import { CalendarHeader } from './CalendarHeader';
import { weekdayNames } from './constants'; import { weekdayNames } from './constants';
import { convertYearFromBackend } from '../StagesTable/utils'; import { isPastDate } from '../utils';
export const Calendar = ({ value, onChange, onClose, isTaskMode = false }) => { const getToday = () => {
const currentYearValue = new Date().getFullYear(); const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
};
const initialDate = isTaskMode const parseValue = (dateString) => {
? value if (!dateString) return null;
? new Date(value) const date = new Date(dateString);
: null return Number.isNaN(date.getTime()) ? null : date;
: convertYearFromBackend(value); };
const initialDisplayYear = initialDate
? initialDate.getFullYear()
: currentYearValue;
const [selectedYear, setSelectedYear] = useState(initialDisplayYear); export const Calendar = ({ value, onChange, onClose }) => {
const initialDate = parseValue(value);
const [currentDate, setCurrentDate] = useState(initialDate || new Date()); const [currentDate, setCurrentDate] = useState(initialDate || new Date());
const [selectedDate, setSelectedDate] = useState(initialDate); const [selectedDate, setSelectedDate] = useState(initialDate);
useEffect(() => { useEffect(() => {
if (value) { const date = parseValue(value);
const date = isTaskMode ? new Date(value) : convertYearFromBackend(value);
if (date) { if (date) {
setCurrentDate(date); setCurrentDate(date);
setSelectedDate(date); setSelectedDate(date);
setSelectedYear(date.getFullYear());
} }
} }, [value]);
}, [value, isTaskMode]);
const handleYearChange = (newYear) => {
setSelectedYear(newYear);
setCurrentDate(new Date(newYear, currentDate.getMonth(), 1));
};
const year = currentDate.getFullYear(); const year = currentDate.getFullYear();
const month = currentDate.getMonth(); const month = currentDate.getMonth();
const firstDayOfMonth = new Date(year, month, 1); const firstDayOfMonth = new Date(year, month, 1);
const lastDayOfMonth = new Date(year, month + 1, 0); const lastDayOfMonth = new Date(year, month + 1, 0);
const firstDayOfWeek = (firstDayOfMonth.getDay() + 6) % 7; // Понедельник = 0 const firstDayOfWeek = (firstDayOfMonth.getDay() + 6) % 7;
const daysInMonth = lastDayOfMonth.getDate(); const daysInMonth = lastDayOfMonth.getDate();
// Нельзя листать раньше января текущего года const today = getToday();
const isPrevMonthDisabled = year === currentYearValue && month === 0; const isPrevMonthDisabled =
year < today.getFullYear() ||
// Нельзя листать дальше декабря следующего года (year === today.getFullYear() && month <= today.getMonth());
const isNextMonthDisabled = year === currentYearValue + 1 && month === 11;
const handlePrevMonth = () => { const handlePrevMonth = () => {
if (!isPrevMonthDisabled) { if (isPrevMonthDisabled) return;
setCurrentDate(new Date(year, month - 1, 1)); setCurrentDate(new Date(year, month - 1, 1));
}
}; };
const handleNextMonth = () => { const handleNextMonth = () => {
if (!isNextMonthDisabled) {
setCurrentDate(new Date(year, month + 1, 1)); setCurrentDate(new Date(year, month + 1, 1));
}
}; };
const handleDateClick = (day) => { const handleDateClick = (day) => {
const newDate = new Date(year, month, day); const newDate = new Date(year, month, day);
if (isPastDate(newDate)) return;
setSelectedDate(newDate); setSelectedDate(newDate);
// Преобразуем год для бэкенда: текущий - 1970, следующий - 1971 const formattedDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
// Для задач используем реальный год
let yearForBackend = year;
if (!isTaskMode) {
if (selectedYear === currentYearValue) {
yearForBackend = 1970;
} else if (selectedYear === currentYearValue + 1) {
yearForBackend = 1971;
}
}
const formattedDate = `${yearForBackend}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
onChange(formattedDate); onChange(formattedDate);
onClose(); onClose();
}; };
const isToday = (day) => { const isToday = (day) =>
const today = new Date();
return (
day === today.getDate() && day === today.getDate() &&
month === today.getMonth() && month === today.getMonth() &&
year === today.getFullYear() year === today.getFullYear();
);
}; const isDayPast = (day) => isPastDate(new Date(year, month, day));
const isSelected = (day) => { const isSelected = (day) => {
if (!selectedDate) return false; if (!selectedDate) return false;
@ -119,14 +96,12 @@ export const Calendar = ({ value, onChange, onClose, isTaskMode = false }) => {
return ( return (
<CalendarContainer> <CalendarContainer>
<YearTabs selectedYear={selectedYear} onYearChange={handleYearChange} />
<CalendarHeader <CalendarHeader
month={month} month={month}
year={year}
onPrevMonth={handlePrevMonth} onPrevMonth={handlePrevMonth}
onNextMonth={handleNextMonth} onNextMonth={handleNextMonth}
isPrevMonthDisabled={isPrevMonthDisabled} isPrevMonthDisabled={isPrevMonthDisabled}
isNextMonthDisabled={isNextMonthDisabled}
/> />
<Box> <Box>
@ -143,6 +118,7 @@ export const Calendar = ({ value, onChange, onClose, isTaskMode = false }) => {
onClick={() => handleDateClick(day)} onClick={() => handleDateClick(day)}
isToday={isToday(day)} isToday={isToday(day)}
isSelected={isSelected(day)} isSelected={isSelected(day)}
isDisabled={isDayPast(day)}
> >
{day} {day}
</CalendarDay> </CalendarDay>

View File

@ -37,7 +37,8 @@ export const CalendarWeekday = styled(Box)`
`; `;
export const CalendarDay = styled(Box, { export const CalendarDay = styled(Box, {
shouldForwardProp: (prop) => prop !== 'isToday' && prop !== 'isSelected', shouldForwardProp: (prop) =>
prop !== 'isToday' && prop !== 'isSelected' && prop !== 'isDisabled',
})` })`
display: flex; display: flex;
align-items: center; align-items: center;
@ -70,4 +71,16 @@ export const CalendarDay = styled(Box, {
background: #258141; background: #258141;
} }
`} `}
${(props) =>
props.isDisabled &&
`
color: #d1d5db;
cursor: not-allowed;
pointer-events: none;
&:hover {
background: transparent;
}
`}
`; `;

View File

@ -6,10 +6,10 @@ import { monthNames } from './constants';
export const CalendarHeader = ({ export const CalendarHeader = ({
month, month,
year,
onPrevMonth, onPrevMonth,
onNextMonth, onNextMonth,
isPrevMonthDisabled, isPrevMonthDisabled,
isNextMonthDisabled,
}) => { }) => {
return ( return (
<StyledCalendarHeader> <StyledCalendarHeader>
@ -20,12 +20,10 @@ export const CalendarHeader = ({
> >
<ChevronLeftIcon /> <ChevronLeftIcon />
</IconButton> </IconButton>
<Typography variant="body1">{monthNames[month]}</Typography> <Typography variant="body1">
<IconButton {monthNames[month]} {year}
size="small" </Typography>
onClick={onNextMonth} <IconButton size="small" onClick={onNextMonth}>
disabled={isNextMonthDisabled}
>
<ChevronRightIcon /> <ChevronRightIcon />
</IconButton> </IconButton>
</StyledCalendarHeader> </StyledCalendarHeader>

View File

@ -9,35 +9,13 @@ import {
} from '@mui/material'; } from '@mui/material';
import { CalendarIcon } from '../../common/icons/icons'; import { CalendarIcon } from '../../common/icons/icons';
import { Calendar } from './Calendar'; import { Calendar } from './Calendar';
import { formatDate } from '../../../utils/formatDate';
export const CalendarInput = ({ value, onChange, isTaskMode = false }) => { export const CalendarInput = ({ value, onChange, isTaskMode = false }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef(null); const containerRef = useRef(null);
const [anchorPosition, setAnchorPosition] = useState(null); const [anchorPosition, setAnchorPosition] = useState(null);
const formatDateForDisplay = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
const currentYear = new Date().getFullYear();
const backendYear = date.getFullYear();
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
if (isTaskMode) {
return `${day}.${month}.${backendYear}`;
}
if (backendYear === 1970 || backendYear === currentYear) {
return `Текущий год: ${day}.${month}`;
} else if (backendYear === 1971 || backendYear === currentYear + 1) {
return `Следующий год: ${day}.${month}`;
} else {
return `${day}.${month}.${backendYear}`;
}
};
const handleOpen = () => { const handleOpen = () => {
if (containerRef.current) { if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
@ -58,7 +36,7 @@ export const CalendarInput = ({ value, onChange, isTaskMode = false }) => {
<> <>
<Box sx={{ position: 'relative' }} ref={containerRef}> <Box sx={{ position: 'relative' }} ref={containerRef}>
<TextField <TextField
value={formatDateForDisplay(value)} value={value ? formatDate(value) : ''}
onClick={handleOpen} onClick={handleOpen}
slotProps={{ slotProps={{
input: { input: {
@ -131,7 +109,6 @@ export const CalendarInput = ({ value, onChange, isTaskMode = false }) => {
value={value} value={value}
onChange={onChange} onChange={onChange}
onClose={handleClose} onClose={handleClose}
isTaskMode={isTaskMode}
/> />
</Box> </Box>
</ClickAwayListener> </ClickAwayListener>

View File

@ -23,7 +23,7 @@ export const ModalContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-end; align-items: stretch;
gap: 1rem; gap: 1rem;
padding: 1.25rem; padding: 1.25rem;
box-sizing: border-box; box-sizing: border-box;

View File

@ -10,7 +10,14 @@ import { useTaskStages } from '../../hooks/useTaskStages';
import { AddStagesModal } from './AddStagesModal'; import { AddStagesModal } from './AddStagesModal';
import { DeleteStageModal } from './DeleteStageModal'; import { DeleteStageModal } from './DeleteStageModal';
import { WhitePlus } from '../common/icons/icons'; import { WhitePlus } from '../common/icons/icons';
import { Button, IconButton, Stack, Typography } from '@mui/material'; import {
Box,
Button,
CircularProgress,
IconButton,
Stack,
Typography,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { StagesTable } from './StagesTable/StagesTable'; import { StagesTable } from './StagesTable/StagesTable';
@ -37,10 +44,10 @@ export const SettingModal = ({
const entityId = mode === 'task' ? taskId : formId; const entityId = mode === 'task' ? taskId : formId;
useEffect(() => { useEffect(() => {
if (entityId) { if (isOpen && entityId) {
getStages(); getStages();
} }
}, [entityId, getStages]); }, [isOpen, entityId, getStages]);
const [isAddEditModalOpen, setIsAddEditModalOpen] = useState(false); const [isAddEditModalOpen, setIsAddEditModalOpen] = useState(false);
const [stageForEdit, setStageForEdit] = useState(null); const [stageForEdit, setStageForEdit] = useState(null);
@ -76,9 +83,12 @@ export const SettingModal = ({
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
children={ children={
<ModalContainer style={{ position: 'relative' }}> <ModalContainer>
<ModalContent> <ModalContent>
<Stack direction="row" justifyContent="flex-end" width="100%"> <Stack
direction="row"
sx={{ width: '100%', justifyContent: 'flex-end' }}
>
<IconButton size="small" onClick={onClose}> <IconButton size="small" onClick={onClose}>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
@ -87,8 +97,7 @@ export const SettingModal = ({
<EditContainer> <EditContainer>
<Stack <Stack
direction="row" direction="row"
justifyContent="space-between" sx={{ width: '100%', justifyContent: 'space-between' }}
alignItems="center"
> >
<Typography fontWeight="bold">Этапы</Typography> <Typography fontWeight="bold">Этапы</Typography>
{!isTaskMode && ( {!isTaskMode && (
@ -102,11 +111,23 @@ export const SettingModal = ({
)} )}
</Stack> </Stack>
{stagesInfo.isLoading ? (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
py: 4,
}}
>
<CircularProgress />
</Box>
) : (
<StagesTable <StagesTable
stages={stagesInfo.stages || []} stages={stagesInfo.stages || []}
onEdit={handleEditStage} onEdit={handleEditStage}
onDelete={!isTaskMode ? handleDeleteStage : undefined} onDelete={!isTaskMode ? handleDeleteStage : undefined}
/> />
)}
<AddStagesModal <AddStagesModal
isOpen={isAddEditModalOpen} isOpen={isAddEditModalOpen}
onClose={() => { onClose={() => {

View File

@ -17,13 +17,10 @@ import {
StyledTableHeadRow, StyledTableHeadRow,
StyledTableRow, StyledTableRow,
} from './StagesTable.style'; } from './StagesTable.style';
import { import { getStageStatus, getStatusColors } from './utils';
getStageStatus, import { formatDate } from '../../../utils/formatDate';
getStatusColors,
convertYearForDisplay,
} from './utils';
import { StageStatusChip } from '../StageStatusChip'; import { StageStatusChip } from '../StageStatusChip';
import { ROLES_NAME_ID } from '../../../constants'; import { ROLES_NAME_ID, STAGE_ROLE_RUSSIAN_NAME } from '../../../constants';
import { useAuth } from '../../../app/context/AuthProvider'; import { useAuth } from '../../../app/context/AuthProvider';
export const StagesTable = ({ stages, onEdit, onDelete }) => { export const StagesTable = ({ stages, onEdit, onDelete }) => {
@ -63,7 +60,7 @@ export const StagesTable = ({ stages, onEdit, onDelete }) => {
<StyledTable> <StyledTable>
<StyledTableHead> <StyledTableHead>
<StyledTableHeadRow> <StyledTableHeadRow>
<TableCell>Название</TableCell> <TableCell>Название таблицы</TableCell>
<TableCell>Начало</TableCell> <TableCell>Начало</TableCell>
<TableCell>Окончание</TableCell> <TableCell>Окончание</TableCell>
<TableCell>Статус</TableCell> <TableCell>Статус</TableCell>
@ -81,22 +78,30 @@ export const StagesTable = ({ stages, onEdit, onDelete }) => {
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
stages.map((stage) => { stages.map((stage, index) => {
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={stage.id}> <StyledTableRow key={rowKey}>
<TableCell> <TableCell>
<Typography variant="body2">{stage.name}</Typography> <Typography variant="body2">{stage.sheet}</Typography>
</TableCell> <Typography
<TableCell> variant="caption"
<Typography variant="body2"> color="text.secondary"
{convertYearForDisplay(stage.period_start)} sx={{ fontSize: '0.65rem' }}
>
{STAGE_ROLE_RUSSIAN_NAME[stage.role] ?? stage.role}
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography variant="body2"> <Typography variant="body2">
{convertYearForDisplay(stage.period_end)} {formatDate(stage.opens_at)}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">
{formatDate(stage.closes_at)}
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>

View File

@ -1,59 +1,26 @@
// Конвертации года из бэкенда (1970/1971) в реальный год
export const convertYearFromBackend = (dateString) => {
if (!dateString) return null;
const date = new Date(dateString);
const backendYear = date.getFullYear();
const currentYear = new Date().getFullYear();
let realYear = backendYear;
if (backendYear === 1970) {
realYear = currentYear;
} else if (backendYear === 1971) {
realYear = currentYear + 1;
}
return new Date(realYear, date.getMonth(), date.getDate());
};
// Форматирование даты для отображения
export const convertYearForDisplay = (dateString) => {
if (!dateString) return '';
const date = convertYearFromBackend(dateString);
if (!date) return '';
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
export const getStageStatus = (stage) => { export const getStageStatus = (stage) => {
if (!stage.period_start || !stage.period_end) { const start = stage.opens_at;
const end = stage.closes_at;
if (!start || !end) {
return 'Не определен'; return 'Не определен';
} }
const now = new Date(); const now = new Date();
now.setHours(0, 0, 0, 0); now.setHours(0, 0, 0, 0);
const startDate = convertYearFromBackend(stage.period_start); const startDate = new Date(start);
const endDate = convertYearFromBackend(stage.period_end); const endDate = new Date(end);
if (!startDate || !endDate) {
return 'Не определен';
}
startDate.setHours(0, 0, 0, 0); startDate.setHours(0, 0, 0, 0);
endDate.setHours(0, 0, 0, 0); endDate.setHours(0, 0, 0, 0);
if (now < startDate) { if (now < startDate) {
return 'Запланирован'; return 'Запланирован';
} else if (now >= startDate && now < endDate) {
return 'Активен';
} else {
return 'Завершен';
} }
if (now >= startDate && now < endDate) {
return 'Активен';
}
return 'Завершен';
}; };
export const getStatusColors = (status) => { export const getStatusColors = (status) => {

View File

@ -6,3 +6,12 @@ export const formatDateForApi = (value) => {
const day = String(d.getDate()).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}; };
export const isPastDate = (value) => {
if (!value) return false;
const date = new Date(value);
date.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
};

View File

@ -10,7 +10,7 @@ const ModalContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-end; align-items: stretch;
padding: 1.25rem; padding: 1.25rem;
box-sizing: border-box; box-sizing: border-box;

View File

@ -1,9 +0,0 @@
// TODO: есть уже похожая функция в utils, может модернизировать ее
export const formatToDDMMYYYY = (dateString) => {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
//у нас год только текущий и следующий, поэтому 1970 считается текущим
const year = date.getFullYear() == 1970 ? 'y' : 'y+1';
return `${day}.${month}.${year}`;
};

View File

@ -18,6 +18,12 @@ export const ROLES_ID_RUSSIAN_NAME_ARRAY = [
{ id: 3, role: 'Исполнитель' }, { id: 3, role: 'Исполнитель' },
]; ];
export const STAGE_ROLE_RUSSIAN_NAME = {
ADMIN: 'Администратор',
DFIP: 'Исполнитель ДФИП',
EXECUTOR_RF: 'Исполнитель',
};
export const TYPES_CELLS = { numeric: 'число', text: 'текст' }; export const TYPES_CELLS = { numeric: 'число', text: 'текст' };
export const EDIT_ACCESS = { 1: 'RESTRICT', 2: 'VIEW', 3: 'EDIT' }; export const EDIT_ACCESS = { 1: 'RESTRICT', 2: 'VIEW', 3: 'EDIT' };

View File

@ -12,15 +12,17 @@ export const useStages = (formId) => {
const getStages = useCallback(() => { const getStages = useCallback(() => {
if (!formId) return; if (!formId) return;
setStagesInfo((prev) => ({ ...prev, isLoading: true }));
StagesApi.getStages(formId) StagesApi.getStages(formId)
.then((data) => { .then((data) => {
setStagesInfo((prev) => ({ setStagesInfo({
...prev, stages: data.result ?? [],
stages: data.result, isLoading: false,
isLoading: true, });
}));
}) })
.catch((error) => { .catch((error) => {
setStagesInfo((prev) => ({ ...prev, isLoading: false }));
toast.error( toast.error(
error?.response?.data?.detail || 'Ошибка при получении этапов', error?.response?.data?.detail || 'Ошибка при получении этапов',
); );
@ -28,12 +30,11 @@ export const useStages = (formId) => {
}, [formId]); }, [formId]);
const createStage = useCallback( const createStage = useCallback(
(stageName, periodStart, periodEnd) => { (periodStart, periodEnd) => {
if (!formId) return Promise.reject('Нет formId'); if (!formId) return Promise.reject('Нет formId');
setIsSaving(true); setIsSaving(true);
const payload = { const payload = {
name: stageName,
period_start: periodStart, period_start: periodStart,
period_end: periodEnd, period_end: periodEnd,
}; };
@ -65,17 +66,18 @@ export const useStages = (formId) => {
if (!formId) return Promise.reject('Нет formId'); if (!formId) return Promise.reject('Нет formId');
setIsSaving(true); setIsSaving(true);
return StagesApi.updateStage(formId, stage)
return StagesApi.updateStage(stage)
.then((data) => { .then((data) => {
setStagesInfo((prev) => { setStagesInfo((prev) => ({
const filterStages = prev.stages.filter( ...prev,
(curStage) => curStage.id !== stage.id, stages: prev.stages.map((curStage) =>
); curStage.sheet === stage.sheet &&
const allStages = [...filterStages, data.result].sort( curStage.phase_code === stage.phase_code
(a, b) => a.id - b.id, ? data.result
); : curStage,
return { ...prev, stages: allStages }; ),
}); }));
toast.success('Данные об этапе обновлены'); toast.success('Данные об этапе обновлены');
return data; return data;
}) })
@ -94,16 +96,21 @@ export const useStages = (formId) => {
const deleteStage = useCallback( const deleteStage = useCallback(
(stage) => { (stage) => {
if (!formId) return; const { sheet, phase_code, budget_form_id } = stage;
if (!budget_form_id || !sheet || !phase_code) return;
StagesApi.deleteStage(formId, stage) StagesApi.deleteStage(budget_form_id, sheet, phase_code)
.then((data) => { .then(() => {
setStagesInfo((prev) => { setStagesInfo((prev) => ({
const filterStages = prev.stages.filter( ...prev,
(curStage) => curStage.id !== stage.id, stages: prev.stages.filter(
); (curStage) =>
return { ...prev, stages: filterStages }; !(
}); curStage.sheet === sheet &&
curStage.phase_code === phase_code
),
),
}));
toast.success('Этап успешно удален'); toast.success('Этап успешно удален');
}) })
.catch((error) => { .catch((error) => {

View File

@ -1,6 +1,6 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { TasksApi } from '../api/tasks';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { StagesApi } from '../api/stages';
export const useTaskStages = (taskId) => { export const useTaskStages = (taskId) => {
const [stagesInfo, setStagesInfo] = useState({ const [stagesInfo, setStagesInfo] = useState({
@ -12,17 +12,17 @@ export const useTaskStages = (taskId) => {
const getStages = useCallback(() => { const getStages = useCallback(() => {
if (!taskId) return; if (!taskId) return;
TasksApi.getTaskStages(taskId) setStagesInfo((prev) => ({ ...prev, isLoading: true }));
StagesApi.getStages(taskId)
.then((data) => { .then((data) => {
if (data.result) { setStagesInfo({
setStagesInfo((prev) => ({ stages: data.result ?? [],
...prev, isLoading: false,
stages: data.result, });
isLoading: true,
}));
}
}) })
.catch((error) => { .catch((error) => {
setStagesInfo((prev) => ({ ...prev, isLoading: false }));
toast.error( toast.error(
error?.response?.data?.detail || 'Ошибка при получении этапов', error?.response?.data?.detail || 'Ошибка при получении этапов',
); );
@ -34,17 +34,18 @@ export const useTaskStages = (taskId) => {
if (!taskId) return Promise.reject('Нет taskId'); if (!taskId) return Promise.reject('Нет taskId');
setIsSaving(true); setIsSaving(true);
return TasksApi.editTaskStages(taskId, stage.id, stage)
return StagesApi.updateStage(stage)
.then((data) => { .then((data) => {
setStagesInfo((prev) => { setStagesInfo((prev) => ({
const filterStages = prev.stages.filter( ...prev,
(curStage) => curStage.id !== stage.id, stages: prev.stages.map((curStage) =>
); curStage.sheet === stage.sheet &&
const allStages = [...filterStages, data.result].sort( curStage.phase_code === stage.phase_code
(a, b) => a.id - b.id, ? data.result
); : curStage,
return { ...prev, stages: allStages }; ),
}); }));
toast.success('Данные об этапе обновлены'); toast.success('Данные об этапе обновлены');
return data; return data;
}) })

View File

@ -10,6 +10,7 @@ import theme from './app/theme/theme';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const baseName = (process.env.REACT_APP_ROOT_PATH || '/').replace(/\/$/, ''); const baseName = (process.env.REACT_APP_ROOT_PATH || '/').replace(/\/$/, '');
@ -31,7 +32,7 @@ createRoot(document.getElementById('root')).render(
}} }}
> >
<App /> <App />
<ToastContainer autoClose={2000} /> <ToastContainer autoClose={2000} style={{ zIndex: 10000 }} />
</BrowserRouter> </BrowserRouter>
</AuthProvider> </AuthProvider>
</LocalizationProvider> </LocalizationProvider>

View File

@ -146,7 +146,7 @@ export default function TaskPage() {
value={query} value={query}
onChange={setQuery} onChange={setQuery}
/> />
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end' }}> <Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end', gap: '.5rem' }}>
<ExportWithTextButton <ExportWithTextButton
onClick={() => exportSingleForm(taskId, task?.title ?? 'form')} onClick={() => exportSingleForm(taskId, task?.title ?? 'form')}
/> />
@ -156,12 +156,14 @@ export default function TaskPage() {
> >
<span>Этапы</span> <span>Этапы</span>
</PrimaryOutlinedButton> </PrimaryOutlinedButton>
{modalStageOpen && (
<SettingStageModal <SettingStageModal
isOpen={modalStageOpen} isOpen={modalStageOpen}
onClose={() => setModalStageOpen(false)} onClose={() => setModalStageOpen(false)}
taskId={taskId} taskId={taskId}
mode="task" mode="task"
/> />
)}
</Stack> </Stack>
</Box> </Box>