Merge pull request 'fix-stages: поправлены ручки по этапам и отображение данных' (#37) from fix-stages into test
Reviewed-on: #37
This commit is contained in:
commit
a206bb7191
@ -2,20 +2,25 @@ import api from './client';
|
||||
|
||||
export const StagesApi = {
|
||||
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
|
||||
.patch(`/forms/${formId}/stages/${stage.id}`, stage)
|
||||
.patch(`/stages/form/${budget_form_id}/${sheet}/${phase_code}`, {
|
||||
opens_at,
|
||||
closes_at,
|
||||
})
|
||||
.then((r) => r.data);
|
||||
},
|
||||
deleteStage(formId, stage) {
|
||||
deleteStage(formId, sheet, phaseCode) {
|
||||
return api
|
||||
.delete(`/forms/${formId}/stages/${stage.id}`)
|
||||
.delete(`/stages/form/${formId}/${sheet}/${phaseCode}`)
|
||||
.then((r) => r.data);
|
||||
},
|
||||
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) {
|
||||
return api
|
||||
|
||||
@ -13,9 +13,6 @@ export const TasksApi = {
|
||||
getById(formId) {
|
||||
return api.get(`/form/${formId}`).then((r) => r.data);
|
||||
},
|
||||
getTaskStages(taskId) {
|
||||
return api.get(`/tasks/${taskId}/stages`).then((r) => r.data);
|
||||
},
|
||||
create(payload) {
|
||||
return api.put("/tasks/", payload).then((r) => r.data);
|
||||
},
|
||||
|
||||
@ -30,42 +30,40 @@ export const AddStagesModal = ({
|
||||
isTaskMode = false,
|
||||
isSaving = false,
|
||||
}) => {
|
||||
const [stageName, setStageName] = useState('');
|
||||
const [startDate, setStartDate] = useState(null);
|
||||
const [endDate, setEndDate] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (stage !== null) {
|
||||
setStageName(stage.name);
|
||||
setStartDate(stage.period_start || null);
|
||||
setEndDate(stage.period_end || null);
|
||||
setStartDate(stage.opens_at ?? null);
|
||||
setEndDate(stage.closes_at ?? null);
|
||||
} else {
|
||||
setStageName('');
|
||||
setStartDate(null);
|
||||
setEndDate(null);
|
||||
}
|
||||
}, [stage, isOpen]);
|
||||
|
||||
const handleSave = () => {
|
||||
const periodStart = formatDateForApi(startDate);
|
||||
const periodEnd = formatDateForApi(endDate);
|
||||
const opensAt = formatDateForApi(startDate);
|
||||
const closesAt = formatDateForApi(endDate);
|
||||
|
||||
if (stage === null) {
|
||||
createStage(stageName, periodStart, periodEnd)?.then(() => {
|
||||
if (!createStage) return;
|
||||
createStage(opensAt, closesAt)?.then(() => {
|
||||
handleCancel();
|
||||
});
|
||||
} else {
|
||||
stage.name = stageName;
|
||||
stage.period_start = periodStart;
|
||||
stage.period_end = periodEnd;
|
||||
updateStage(stage)?.then(() => {
|
||||
updateStage({
|
||||
...stage,
|
||||
opens_at: opensAt,
|
||||
closes_at: closesAt,
|
||||
})?.then(() => {
|
||||
handleCancel();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setStageName('');
|
||||
setStartDate(null);
|
||||
setEndDate(null);
|
||||
onClose();
|
||||
@ -77,7 +75,7 @@ export const AddStagesModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid = stageName && startDate && endDate;
|
||||
const isFormValid = startDate && endDate;
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@ -88,7 +86,7 @@ export const AddStagesModal = ({
|
||||
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
|
||||
<ModalContainer>
|
||||
<HeaderContainer>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<ModalTitle>
|
||||
{stage ? 'Редактирование этапа' : 'Добавление этапа'}
|
||||
</ModalTitle>
|
||||
@ -108,22 +106,6 @@ export const AddStagesModal = ({
|
||||
|
||||
<Box>
|
||||
<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
|
||||
direction="row"
|
||||
|
||||
@ -6,97 +6,74 @@ import {
|
||||
CalendarDay,
|
||||
CalendarWeekday,
|
||||
} from './Calendar.styles';
|
||||
import { YearTabs } from './YearTabs';
|
||||
import { CalendarHeader } from './CalendarHeader';
|
||||
import { weekdayNames } from './constants';
|
||||
import { convertYearFromBackend } from '../StagesTable/utils';
|
||||
import { isPastDate } from '../utils';
|
||||
|
||||
export const Calendar = ({ value, onChange, onClose, isTaskMode = false }) => {
|
||||
const currentYearValue = new Date().getFullYear();
|
||||
const getToday = () => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today;
|
||||
};
|
||||
|
||||
const initialDate = isTaskMode
|
||||
? value
|
||||
? new Date(value)
|
||||
: null
|
||||
: convertYearFromBackend(value);
|
||||
const initialDisplayYear = initialDate
|
||||
? initialDate.getFullYear()
|
||||
: currentYearValue;
|
||||
const parseValue = (dateString) => {
|
||||
if (!dateString) return null;
|
||||
const date = new Date(dateString);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
};
|
||||
|
||||
const [selectedYear, setSelectedYear] = useState(initialDisplayYear);
|
||||
export const Calendar = ({ value, onChange, onClose }) => {
|
||||
const initialDate = parseValue(value);
|
||||
const [currentDate, setCurrentDate] = useState(initialDate || new Date());
|
||||
const [selectedDate, setSelectedDate] = useState(initialDate);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const date = isTaskMode ? new Date(value) : convertYearFromBackend(value);
|
||||
if (date) {
|
||||
setCurrentDate(date);
|
||||
setSelectedDate(date);
|
||||
setSelectedYear(date.getFullYear());
|
||||
}
|
||||
const date = parseValue(value);
|
||||
if (date) {
|
||||
setCurrentDate(date);
|
||||
setSelectedDate(date);
|
||||
}
|
||||
}, [value, isTaskMode]);
|
||||
|
||||
const handleYearChange = (newYear) => {
|
||||
setSelectedYear(newYear);
|
||||
setCurrentDate(new Date(newYear, currentDate.getMonth(), 1));
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
|
||||
const firstDayOfMonth = new Date(year, month, 1);
|
||||
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 isPrevMonthDisabled = year === currentYearValue && month === 0;
|
||||
|
||||
// Нельзя листать дальше декабря следующего года
|
||||
const isNextMonthDisabled = year === currentYearValue + 1 && month === 11;
|
||||
const today = getToday();
|
||||
const isPrevMonthDisabled =
|
||||
year < today.getFullYear() ||
|
||||
(year === today.getFullYear() && month <= today.getMonth());
|
||||
|
||||
const handlePrevMonth = () => {
|
||||
if (!isPrevMonthDisabled) {
|
||||
setCurrentDate(new Date(year, month - 1, 1));
|
||||
}
|
||||
if (isPrevMonthDisabled) return;
|
||||
setCurrentDate(new Date(year, month - 1, 1));
|
||||
};
|
||||
|
||||
const handleNextMonth = () => {
|
||||
if (!isNextMonthDisabled) {
|
||||
setCurrentDate(new Date(year, month + 1, 1));
|
||||
}
|
||||
setCurrentDate(new Date(year, month + 1, 1));
|
||||
};
|
||||
|
||||
const handleDateClick = (day) => {
|
||||
const newDate = new Date(year, month, day);
|
||||
if (isPastDate(newDate)) return;
|
||||
|
||||
setSelectedDate(newDate);
|
||||
|
||||
// Преобразуем год для бэкенда: текущий - 1970, следующий - 1971
|
||||
// Для задач используем реальный год
|
||||
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')}`;
|
||||
const formattedDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
onChange(formattedDate);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isToday = (day) => {
|
||||
const today = new Date();
|
||||
return (
|
||||
day === today.getDate() &&
|
||||
month === today.getMonth() &&
|
||||
year === today.getFullYear()
|
||||
);
|
||||
};
|
||||
const isToday = (day) =>
|
||||
day === today.getDate() &&
|
||||
month === today.getMonth() &&
|
||||
year === today.getFullYear();
|
||||
|
||||
const isDayPast = (day) => isPastDate(new Date(year, month, day));
|
||||
|
||||
const isSelected = (day) => {
|
||||
if (!selectedDate) return false;
|
||||
@ -119,14 +96,12 @@ export const Calendar = ({ value, onChange, onClose, isTaskMode = false }) => {
|
||||
|
||||
return (
|
||||
<CalendarContainer>
|
||||
<YearTabs selectedYear={selectedYear} onYearChange={handleYearChange} />
|
||||
|
||||
<CalendarHeader
|
||||
month={month}
|
||||
year={year}
|
||||
onPrevMonth={handlePrevMonth}
|
||||
onNextMonth={handleNextMonth}
|
||||
isPrevMonthDisabled={isPrevMonthDisabled}
|
||||
isNextMonthDisabled={isNextMonthDisabled}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
@ -143,6 +118,7 @@ export const Calendar = ({ value, onChange, onClose, isTaskMode = false }) => {
|
||||
onClick={() => handleDateClick(day)}
|
||||
isToday={isToday(day)}
|
||||
isSelected={isSelected(day)}
|
||||
isDisabled={isDayPast(day)}
|
||||
>
|
||||
{day}
|
||||
</CalendarDay>
|
||||
|
||||
@ -37,7 +37,8 @@ export const CalendarWeekday = styled(Box)`
|
||||
`;
|
||||
|
||||
export const CalendarDay = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== 'isToday' && prop !== 'isSelected',
|
||||
shouldForwardProp: (prop) =>
|
||||
prop !== 'isToday' && prop !== 'isSelected' && prop !== 'isDisabled',
|
||||
})`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -70,4 +71,16 @@ export const CalendarDay = styled(Box, {
|
||||
background: #258141;
|
||||
}
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.isDisabled &&
|
||||
`
|
||||
color: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
@ -6,10 +6,10 @@ import { monthNames } from './constants';
|
||||
|
||||
export const CalendarHeader = ({
|
||||
month,
|
||||
year,
|
||||
onPrevMonth,
|
||||
onNextMonth,
|
||||
isPrevMonthDisabled,
|
||||
isNextMonthDisabled,
|
||||
}) => {
|
||||
return (
|
||||
<StyledCalendarHeader>
|
||||
@ -20,12 +20,10 @@ export const CalendarHeader = ({
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</IconButton>
|
||||
<Typography variant="body1">{monthNames[month]}</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onNextMonth}
|
||||
disabled={isNextMonthDisabled}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
{monthNames[month]} {year}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={onNextMonth}>
|
||||
<ChevronRightIcon />
|
||||
</IconButton>
|
||||
</StyledCalendarHeader>
|
||||
|
||||
@ -9,35 +9,13 @@ import {
|
||||
} from '@mui/material';
|
||||
import { CalendarIcon } from '../../common/icons/icons';
|
||||
import { Calendar } from './Calendar';
|
||||
import { formatDate } from '../../../utils/formatDate';
|
||||
|
||||
export const CalendarInput = ({ value, onChange, isTaskMode = false }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef(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 = () => {
|
||||
if (containerRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
@ -58,7 +36,7 @@ export const CalendarInput = ({ value, onChange, isTaskMode = false }) => {
|
||||
<>
|
||||
<Box sx={{ position: 'relative' }} ref={containerRef}>
|
||||
<TextField
|
||||
value={formatDateForDisplay(value)}
|
||||
value={value ? formatDate(value) : ''}
|
||||
onClick={handleOpen}
|
||||
slotProps={{
|
||||
input: {
|
||||
@ -131,7 +109,6 @@ export const CalendarInput = ({ value, onChange, isTaskMode = false }) => {
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onClose={handleClose}
|
||||
isTaskMode={isTaskMode}
|
||||
/>
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
|
||||
@ -23,7 +23,7 @@ export const ModalContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
@ -10,7 +10,14 @@ import { useTaskStages } from '../../hooks/useTaskStages';
|
||||
import { AddStagesModal } from './AddStagesModal';
|
||||
import { DeleteStageModal } from './DeleteStageModal';
|
||||
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 { StagesTable } from './StagesTable/StagesTable';
|
||||
|
||||
@ -37,10 +44,10 @@ export const SettingModal = ({
|
||||
const entityId = mode === 'task' ? taskId : formId;
|
||||
|
||||
useEffect(() => {
|
||||
if (entityId) {
|
||||
if (isOpen && entityId) {
|
||||
getStages();
|
||||
}
|
||||
}, [entityId, getStages]);
|
||||
}, [isOpen, entityId, getStages]);
|
||||
|
||||
const [isAddEditModalOpen, setIsAddEditModalOpen] = useState(false);
|
||||
const [stageForEdit, setStageForEdit] = useState(null);
|
||||
@ -76,9 +83,12 @@ export const SettingModal = ({
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
children={
|
||||
<ModalContainer style={{ position: 'relative' }}>
|
||||
<ModalContainer>
|
||||
<ModalContent>
|
||||
<Stack direction="row" justifyContent="flex-end" width="100%">
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{ width: '100%', justifyContent: 'flex-end' }}
|
||||
>
|
||||
<IconButton size="small" onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
@ -87,8 +97,7 @@ export const SettingModal = ({
|
||||
<EditContainer>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', justifyContent: 'space-between' }}
|
||||
>
|
||||
<Typography fontWeight="bold">Этапы</Typography>
|
||||
{!isTaskMode && (
|
||||
@ -102,11 +111,23 @@ export const SettingModal = ({
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<StagesTable
|
||||
stages={stagesInfo.stages || []}
|
||||
onEdit={handleEditStage}
|
||||
onDelete={!isTaskMode ? handleDeleteStage : undefined}
|
||||
/>
|
||||
{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={() => {
|
||||
|
||||
@ -17,13 +17,10 @@ import {
|
||||
StyledTableHeadRow,
|
||||
StyledTableRow,
|
||||
} from './StagesTable.style';
|
||||
import {
|
||||
getStageStatus,
|
||||
getStatusColors,
|
||||
convertYearForDisplay,
|
||||
} from './utils';
|
||||
import { getStageStatus, getStatusColors } from './utils';
|
||||
import { formatDate } from '../../../utils/formatDate';
|
||||
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';
|
||||
|
||||
export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
||||
@ -63,7 +60,7 @@ export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
||||
<StyledTable>
|
||||
<StyledTableHead>
|
||||
<StyledTableHeadRow>
|
||||
<TableCell>Название</TableCell>
|
||||
<TableCell>Название таблицы</TableCell>
|
||||
<TableCell>Начало</TableCell>
|
||||
<TableCell>Окончание</TableCell>
|
||||
<TableCell>Статус</TableCell>
|
||||
@ -81,22 +78,30 @@ export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
stages.map((stage) => {
|
||||
stages.map((stage, index) => {
|
||||
const status = getStageStatus(stage);
|
||||
const colors = getStatusColors(status);
|
||||
const rowKey = `${stage.sheet}-${stage.phase_code}-${stage.role}-${index}`;
|
||||
return (
|
||||
<StyledTableRow key={stage.id}>
|
||||
<StyledTableRow key={rowKey}>
|
||||
<TableCell>
|
||||
<Typography variant="body2">{stage.name}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{convertYearForDisplay(stage.period_start)}
|
||||
<Typography variant="body2">{stage.sheet}</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.65rem' }}
|
||||
>
|
||||
{STAGE_ROLE_RUSSIAN_NAME[stage.role] ?? stage.role}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{convertYearForDisplay(stage.period_end)}
|
||||
{formatDate(stage.opens_at)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatDate(stage.closes_at)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@ -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) => {
|
||||
if (!stage.period_start || !stage.period_end) {
|
||||
const start = stage.opens_at;
|
||||
const end = stage.closes_at;
|
||||
|
||||
if (!start || !end) {
|
||||
return 'Не определен';
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
const startDate = convertYearFromBackend(stage.period_start);
|
||||
const endDate = convertYearFromBackend(stage.period_end);
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return 'Не определен';
|
||||
}
|
||||
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (now < startDate) {
|
||||
return 'Запланирован';
|
||||
} else if (now >= startDate && now < endDate) {
|
||||
return 'Активен';
|
||||
} else {
|
||||
return 'Завершен';
|
||||
}
|
||||
if (now >= startDate && now < endDate) {
|
||||
return 'Активен';
|
||||
}
|
||||
return 'Завершен';
|
||||
};
|
||||
|
||||
export const getStatusColors = (status) => {
|
||||
|
||||
@ -6,3 +6,12 @@ export const formatDateForApi = (value) => {
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ const ModalContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
align-items: stretch;
|
||||
padding: 1.25rem;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
@ -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}`;
|
||||
};
|
||||
@ -18,6 +18,12 @@ export const ROLES_ID_RUSSIAN_NAME_ARRAY = [
|
||||
{ id: 3, role: 'Исполнитель' },
|
||||
];
|
||||
|
||||
export const STAGE_ROLE_RUSSIAN_NAME = {
|
||||
ADMIN: 'Администратор',
|
||||
DFIP: 'Исполнитель ДФИП',
|
||||
EXECUTOR_RF: 'Исполнитель',
|
||||
};
|
||||
|
||||
export const TYPES_CELLS = { numeric: 'число', text: 'текст' };
|
||||
|
||||
export const EDIT_ACCESS = { 1: 'RESTRICT', 2: 'VIEW', 3: 'EDIT' };
|
||||
|
||||
@ -12,15 +12,17 @@ export const useStages = (formId) => {
|
||||
const getStages = useCallback(() => {
|
||||
if (!formId) return;
|
||||
|
||||
setStagesInfo((prev) => ({ ...prev, isLoading: true }));
|
||||
|
||||
StagesApi.getStages(formId)
|
||||
.then((data) => {
|
||||
setStagesInfo((prev) => ({
|
||||
...prev,
|
||||
stages: data.result,
|
||||
isLoading: true,
|
||||
}));
|
||||
setStagesInfo({
|
||||
stages: data.result ?? [],
|
||||
isLoading: false,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setStagesInfo((prev) => ({ ...prev, isLoading: false }));
|
||||
toast.error(
|
||||
error?.response?.data?.detail || 'Ошибка при получении этапов',
|
||||
);
|
||||
@ -28,12 +30,11 @@ export const useStages = (formId) => {
|
||||
}, [formId]);
|
||||
|
||||
const createStage = useCallback(
|
||||
(stageName, periodStart, periodEnd) => {
|
||||
(periodStart, periodEnd) => {
|
||||
if (!formId) return Promise.reject('Нет formId');
|
||||
|
||||
setIsSaving(true);
|
||||
const payload = {
|
||||
name: stageName,
|
||||
period_start: periodStart,
|
||||
period_end: periodEnd,
|
||||
};
|
||||
@ -65,17 +66,18 @@ export const useStages = (formId) => {
|
||||
if (!formId) return Promise.reject('Нет formId');
|
||||
|
||||
setIsSaving(true);
|
||||
return StagesApi.updateStage(formId, stage)
|
||||
|
||||
return StagesApi.updateStage(stage)
|
||||
.then((data) => {
|
||||
setStagesInfo((prev) => {
|
||||
const filterStages = prev.stages.filter(
|
||||
(curStage) => curStage.id !== stage.id,
|
||||
);
|
||||
const allStages = [...filterStages, data.result].sort(
|
||||
(a, b) => a.id - b.id,
|
||||
);
|
||||
return { ...prev, stages: allStages };
|
||||
});
|
||||
setStagesInfo((prev) => ({
|
||||
...prev,
|
||||
stages: prev.stages.map((curStage) =>
|
||||
curStage.sheet === stage.sheet &&
|
||||
curStage.phase_code === stage.phase_code
|
||||
? data.result
|
||||
: curStage,
|
||||
),
|
||||
}));
|
||||
toast.success('Данные об этапе обновлены');
|
||||
return data;
|
||||
})
|
||||
@ -94,16 +96,21 @@ export const useStages = (formId) => {
|
||||
|
||||
const deleteStage = useCallback(
|
||||
(stage) => {
|
||||
if (!formId) return;
|
||||
const { sheet, phase_code, budget_form_id } = stage;
|
||||
if (!budget_form_id || !sheet || !phase_code) return;
|
||||
|
||||
StagesApi.deleteStage(formId, stage)
|
||||
.then((data) => {
|
||||
setStagesInfo((prev) => {
|
||||
const filterStages = prev.stages.filter(
|
||||
(curStage) => curStage.id !== stage.id,
|
||||
);
|
||||
return { ...prev, stages: filterStages };
|
||||
});
|
||||
StagesApi.deleteStage(budget_form_id, sheet, phase_code)
|
||||
.then(() => {
|
||||
setStagesInfo((prev) => ({
|
||||
...prev,
|
||||
stages: prev.stages.filter(
|
||||
(curStage) =>
|
||||
!(
|
||||
curStage.sheet === sheet &&
|
||||
curStage.phase_code === phase_code
|
||||
),
|
||||
),
|
||||
}));
|
||||
toast.success('Этап успешно удален');
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { TasksApi } from '../api/tasks';
|
||||
import { toast } from 'react-toastify';
|
||||
import { StagesApi } from '../api/stages';
|
||||
|
||||
export const useTaskStages = (taskId) => {
|
||||
const [stagesInfo, setStagesInfo] = useState({
|
||||
@ -12,17 +12,17 @@ export const useTaskStages = (taskId) => {
|
||||
const getStages = useCallback(() => {
|
||||
if (!taskId) return;
|
||||
|
||||
TasksApi.getTaskStages(taskId)
|
||||
setStagesInfo((prev) => ({ ...prev, isLoading: true }));
|
||||
|
||||
StagesApi.getStages(taskId)
|
||||
.then((data) => {
|
||||
if (data.result) {
|
||||
setStagesInfo((prev) => ({
|
||||
...prev,
|
||||
stages: data.result,
|
||||
isLoading: true,
|
||||
}));
|
||||
}
|
||||
setStagesInfo({
|
||||
stages: data.result ?? [],
|
||||
isLoading: false,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setStagesInfo((prev) => ({ ...prev, isLoading: false }));
|
||||
toast.error(
|
||||
error?.response?.data?.detail || 'Ошибка при получении этапов',
|
||||
);
|
||||
@ -34,17 +34,18 @@ export const useTaskStages = (taskId) => {
|
||||
if (!taskId) return Promise.reject('Нет taskId');
|
||||
|
||||
setIsSaving(true);
|
||||
return TasksApi.editTaskStages(taskId, stage.id, stage)
|
||||
|
||||
return StagesApi.updateStage(stage)
|
||||
.then((data) => {
|
||||
setStagesInfo((prev) => {
|
||||
const filterStages = prev.stages.filter(
|
||||
(curStage) => curStage.id !== stage.id,
|
||||
);
|
||||
const allStages = [...filterStages, data.result].sort(
|
||||
(a, b) => a.id - b.id,
|
||||
);
|
||||
return { ...prev, stages: allStages };
|
||||
});
|
||||
setStagesInfo((prev) => ({
|
||||
...prev,
|
||||
stages: prev.stages.map((curStage) =>
|
||||
curStage.sheet === stage.sheet &&
|
||||
curStage.phase_code === stage.phase_code
|
||||
? data.result
|
||||
: curStage,
|
||||
),
|
||||
}));
|
||||
toast.success('Данные об этапе обновлены');
|
||||
return data;
|
||||
})
|
||||
|
||||
@ -10,6 +10,7 @@ import theme from './app/theme/theme';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
const baseName = (process.env.REACT_APP_ROOT_PATH || '/').replace(/\/$/, '');
|
||||
|
||||
@ -31,7 +32,7 @@ createRoot(document.getElementById('root')).render(
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
<ToastContainer autoClose={2000} />
|
||||
<ToastContainer autoClose={2000} style={{ zIndex: 10000 }} />
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
</LocalizationProvider>
|
||||
|
||||
@ -146,7 +146,7 @@ export default function TaskPage() {
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
/>
|
||||
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end' }}>
|
||||
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end', gap: '.5rem' }}>
|
||||
<ExportWithTextButton
|
||||
onClick={() => exportSingleForm(taskId, task?.title ?? 'form')}
|
||||
/>
|
||||
@ -156,12 +156,14 @@ export default function TaskPage() {
|
||||
>
|
||||
<span>Этапы</span>
|
||||
</PrimaryOutlinedButton>
|
||||
<SettingStageModal
|
||||
isOpen={modalStageOpen}
|
||||
onClose={() => setModalStageOpen(false)}
|
||||
taskId={taskId}
|
||||
mode="task"
|
||||
/>
|
||||
{modalStageOpen && (
|
||||
<SettingStageModal
|
||||
isOpen={modalStageOpen}
|
||||
onClose={() => setModalStageOpen(false)}
|
||||
taskId={taskId}
|
||||
mode="task"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user