Merge pull request '[AURORA-1043] - создание задач' (#42) from AURORA-1043 into test

Reviewed-on: #42
This commit is contained in:
Veronika 2026-06-08 16:48:55 +03:00
commit 96477df9e3
7 changed files with 259 additions and 84 deletions

View File

@ -14,7 +14,7 @@ export const TasksApi = {
return api.get(`/form/${formId}`).then((r) => r.data); return api.get(`/form/${formId}`).then((r) => r.data);
}, },
create(payload) { create(payload) {
return api.put("/tasks/", payload).then((r) => r.data); return api.post("/form/", payload).then((r) => r.data);
}, },
editTask(taskId, payload = {}) { editTask(taskId, payload = {}) {
return api.patch(`/tasks/${taskId}`, payload).then((r) => r.data); return api.patch(`/tasks/${taskId}`, payload).then((r) => r.data);

View File

@ -1,7 +1,7 @@
import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
import { useAuth } from './context/AuthProvider'; import { useAuth } from './context/AuthProvider';
import { Layout } from './layout/Layout'; import { Layout } from './layout/Layout';
import TasksPage from '../pages/TasksPage'; import TasksPage from '../pages/TasksPage/TasksPage';
import FormsPage from '../pages/FormsPage'; import FormsPage from '../pages/FormsPage';
import TaskPage from '../pages/TaskPage/TaskPage'; import TaskPage from '../pages/TaskPage/TaskPage';
import FormPage from '../pages/FormPage'; import FormPage from '../pages/FormPage';
@ -39,7 +39,6 @@ export const AppRoutes = () => {
<Route path="/tasks" element={<TasksPage />} /> <Route path="/tasks" element={<TasksPage />} />
<Route path="/forms" element={<FormsPage />} /> <Route path="/forms" element={<FormsPage />} />
<Route path="/task/:taskId" element={<TaskPage />} /> <Route path="/task/:taskId" element={<TaskPage />} />
<Route path="/form/:formId" element={<FormPage />} />
<Route path="/table/:tableId" element={<TablePage />} /> <Route path="/table/:tableId" element={<TablePage />} />
<Route path="/table-temp/:tableId" element={<TableTempPage />} /> <Route path="/table-temp/:tableId" element={<TableTempPage />} />
<Route path="/admin_panel/users" element={<UsersPage />} /> <Route path="/admin_panel/users" element={<UsersPage />} />

View File

@ -75,7 +75,7 @@ export function HeaderSwitch() {
} }
></TasksSvg> ></TasksSvg>
</div> </div>
<span>Задачи и формы</span> <span>Задачи</span>
</StyledNavLink> </StyledNavLink>
{user.role_id === ROLES_NAME_ID.admin && ( {user.role_id === ROLES_NAME_ID.admin && (
<StyledNavLink <StyledNavLink

View File

@ -35,6 +35,13 @@ export const EDIT_ACCESS_RUSSIAN = {
3: 'Просмотр и редактирование', 3: 'Просмотр и редактирование',
}; };
export const FORM_TYPE_OPTIONS = ['FORM_1', 'FORM_2', 'FORM_3', 'FORM_4'];
export const ORG_UNIT_TYPE_OPTIONS = [
{ value: 'ssp', label: 'ССП' },
{ value: 'rf', label: 'Региональный филиал' },
];
export const SHEET_NAME = { export const SHEET_NAME = {
"AHR": "АХР", "AHR": "АХР",
"CAP": "КВП", "CAP": "КВП",

View File

@ -0,0 +1,46 @@
import { forwardRef } from 'react';
import { Box, Paper, Typography } from '@mui/material';
import { CustomCheckbox } from '../../components/common/StyledCheckbox';
export const OrgUnitsAutocompletePaper = forwardRef(
function OrgUnitsAutocompletePaper(
{
children,
onSelectAll,
isAllSelected,
isIndeterminate,
showSelectAll,
...other
},
ref,
) {
return (
<Paper ref={ref} {...other}>
{showSelectAll && (
<Box
onMouseDown={(event) => event.preventDefault()}
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
px: 2,
py: 1,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<CustomCheckbox
checked={isAllSelected}
indeterminate={isIndeterminate}
onChange={(event) => onSelectAll(event.target.checked)}
/>
<Typography variant="body2" sx={{ fontSize: '0.875rem' }}>
Выделить все
</Typography>
</Box>
)}
{children}
</Paper>
);
},
);

View File

@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { TasksApi } from '../api/tasks'; import { TasksApi } from '../../api/tasks';
import { CardsSkeleton } from '../components/common/Skeleton'; import { CardsSkeleton } from '../../components/common/Skeleton';
import { SwitchFormTask } from '../components/common/SwitchFormTask/SwitchFormTask'; import { SwitchFormTask } from '../../components/common/SwitchFormTask/SwitchFormTask';
import SearchComponent from '../components/common/SearchComponent'; import SearchComponent from '../../components/common/SearchComponent';
import { TaskCardComponent } from '../components/common/TaskCardComponent'; import { TaskCardComponent } from '../../components/common/TaskCardComponent';
import { HeaderSwitch } from '../components/HeaderMenu/HeaderSwitch'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { PageContainer, TasksContainer } from './StyledForPage'; import { PageContainer, TasksContainer } from '../StyledForPage';
import { import {
Box, Box,
FormControl, FormControl,
@ -21,18 +21,34 @@ import {
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Modal from '../components/common/Modal/Modal'; import Modal from '../../components/common/Modal/Modal';
import { FormsApi } from '../api/forms'; import {
import { ROLES_NAME_ID } from '../constants'; FORM_TYPE_OPTIONS,
import { useAuth } from '../app/context/AuthProvider'; ORG_UNIT_TYPE_OPTIONS,
import { SspApi } from '../api/ssp'; ROLES_NAME_ID,
import { PrimaryButton } from '../components/common/Buttons/Buttons'; } from '../../constants';
import { useAuth } from '../../app/context/AuthProvider';
import { SspApi } from '../../api/ssp';
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
import { import {
ExportExitButton, ExportExitButton,
ExportWithTextButton, ExportWithTextButton,
} from '../components/common/Buttons/ButtonsActions'; } from '../../components/common/Buttons/ButtonsActions';
import { exportBulkForms, exportSingleForm } from '../utils/exportForm'; import { exportBulkForms, exportSingleForm } from '../../utils/exportForm';
import { WhitePlus } from '../components/common/icons/icons'; import { WhitePlus } from '../../components/common/icons/icons';
import { OrgUnitsAutocompletePaper } from './OrgUnitsAutocompletePaper';
import { renderOrgUnitValue } from './renderOrgUnitValue';
const modalSelectMenuProps = {
sx: { zIndex: 1301 },
style: { zIndex: 1301 },
};
const modalSelectSx = {
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'rgba(0,0,0,0.1)',
},
};
export default function TasksPage() { export default function TasksPage() {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
@ -48,14 +64,30 @@ export default function TasksPage() {
const [isLoadAll, setisLoadAll] = useState(false); const [isLoadAll, setisLoadAll] = useState(false);
const [sspList, setSspList] = useState([]); const [sspList, setSspList] = useState([]);
const [forms, setForms] = useState([]);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
title: '',
year: '', year: '',
sspIds: [], orgUnitType: '',
formId: '', org_unit_id: [],
formTypeCode: '',
}); });
const filteredOrgUnits = useMemo(() => {
if (!formData.orgUnitType) return [];
const isSsp = formData.orgUnitType === 'ssp';
return sspList.filter((unit) => unit.is_ssp === isSsp);
}, [sspList, formData.orgUnitType]);
const isAllOrgUnitsSelected = useMemo(() => {
return (
filteredOrgUnits.length > 0 &&
filteredOrgUnits.every((unit) => formData.org_unit_id.includes(unit.id))
);
}, [filteredOrgUnits, formData.org_unit_id]);
const isOrgUnitsIndeterminate = useMemo(() => {
return formData.org_unit_id.length > 0 && !isAllOrgUnitsSelected;
}, [formData.org_unit_id, isAllOrgUnitsSelected]);
const { user } = useAuth(); const { user } = useAuth();
const [exportMode, setExportMode] = useState(false); const [exportMode, setExportMode] = useState(false);
@ -66,34 +98,40 @@ export default function TasksPage() {
const handleSubmit = async () => { const handleSubmit = async () => {
if ( if (
!formData.title ||
!formData.year || !formData.year ||
formData.sspIds.length === 0 || !formData.orgUnitType ||
!formData.formId formData.org_unit_id.length === 0 ||
!formData.formTypeCode
) { ) {
alert('Пожалуйста, заполните все поля'); alert('Пожалуйста, заполните все поля');
return; return;
} }
try { try {
const taskData = {
title: formData.title,
ssp_ids: formData.sspIds,
form_id: formData.formId,
start_year: Number(formData.year),
};
setLoadingUpdate(true); setLoadingUpdate(true);
await TasksApi.create(taskData); await TasksApi.create({
year: Number(formData.year),
form_type_code: formData.formTypeCode,
org_unit_ids: formData.org_unit_id,
});
// Reset to first page after creating a new task
setPage(1); setPage(1);
loadTasks(1, limit); loadTasks(1, limit);
setModalOpen(false); setModalOpen(false);
setFormData({ title: '', year: '', sspIds: [], formId: '' }); setFormData({
year: '',
orgUnitType: '',
org_unit_id: [],
formTypeCode: '',
});
} catch (error) { } catch (error) {
toast.error(error.response.data.detail || 'Ошибка при создании задачи'); toast.error(
error.response?.data?.detail ||
error.response?.data?.message ||
'Ошибка при создании задачи',
);
} finally { } finally {
setLoadingUpdate(false); setLoadingUpdate(false);
} }
@ -101,7 +139,12 @@ export default function TasksPage() {
const handleCancel = () => { const handleCancel = () => {
setModalOpen(false); setModalOpen(false);
setFormData({ title: '', year: '', sspIds: [], formId: '' }); setFormData({
year: '',
orgUnitType: '',
org_unit_id: [],
formTypeCode: '',
});
}; };
const handleFormChange = (field, value) => { const handleFormChange = (field, value) => {
@ -111,6 +154,21 @@ export default function TasksPage() {
})); }));
}; };
const handleOrgUnitTypeChange = (value) => {
setFormData((prev) => ({
...prev,
orgUnitType: value,
org_unit_id: [],
}));
};
const handleSelectAllOrgUnits = (checked) => {
handleFormChange(
'org_unit_id',
checked ? filteredOrgUnits.map((unit) => unit.id) : [],
);
};
const loadTasks = async (currentPage = page, currentLimit = limit) => { const loadTasks = async (currentPage = page, currentLimit = limit) => {
if (isLoadAll) return; if (isLoadAll) return;
setLoading(true); setLoading(true);
@ -158,11 +216,6 @@ export default function TasksPage() {
setSspList(data.result); setSspList(data.result);
} }
}); });
FormsApi.formsList().then((data) => {
if (data) {
setForms(data);
}
});
} }
}, [modalOpen]); }, [modalOpen]);
@ -350,41 +403,30 @@ export default function TasksPage() {
} }
> >
<div className="space-y-4"> <div className="space-y-4">
<FormControl fullWidth>
<FormLabel required>Название задачи</FormLabel>
{/* <TextField
size="small"
placeholder="Укажите название задачи"
value={formData.title}
onChange={(e) => handleFormChange('title', e.target.value)}
/> */}
</FormControl>
<FormControl fullWidth size="small"> <FormControl fullWidth size="small">
<FormLabel required>Форма</FormLabel> <FormLabel required>Форма</FormLabel>
<Autocomplete <Select
size="small" size="small"
options={forms} displayEmpty
noOptionsText="Не найдено" fullWidth
getOptionLabel={(option) => option.title || option.name || option.form_type_code || ''} value={formData.formTypeCode}
value={forms.find((f) => f.id === formData.formId) || null} onChange={(e) => handleFormChange('formTypeCode', e.target.value)}
onChange={(event, newValue) => { renderValue={(selected) =>
handleFormChange('formId', newValue ? newValue.id : ''); selected ? (
}} selected
renderInput={(params) => ( ) : (
<TextField <span style={{ color: 'rgba(0,0,0,0.6)' }}>Не выбрана</span>
{...params} )
placeholder="Не выбрана" }
sx={{ MenuProps={modalSelectMenuProps}
'& .MuiOutlinedInput-root': { sx={modalSelectSx}
'& fieldset': { >
borderColor: 'rgba(0,0,0,0.1)', {FORM_TYPE_OPTIONS.map((formType) => (
}, <MenuItem key={formType} value={formType}>
}, {formType}
}} </MenuItem>
/> ))}
)} </Select>
/>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
@ -398,26 +440,60 @@ export default function TasksPage() {
/> />
</FormControl> </FormControl>
<FormControl fullWidth size="small">
<FormLabel required>Тип</FormLabel>
<Select
size="small"
displayEmpty
fullWidth
value={formData.orgUnitType}
onChange={(e) => handleOrgUnitTypeChange(e.target.value)}
renderValue={(selected) => {
const option = ORG_UNIT_TYPE_OPTIONS.find(
(item) => item.value === selected,
);
return option ? (
option.label
) : (
<span style={{ color: 'rgba(0,0,0,0.6)' }}>Не выбран</span>
);
}}
MenuProps={modalSelectMenuProps}
sx={modalSelectSx}
>
{ORG_UNIT_TYPE_OPTIONS.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth size="small"> <FormControl fullWidth size="small">
<FormLabel required>ССП/Региональный филиал</FormLabel> <FormLabel required>ССП/Региональный филиал</FormLabel>
{/* <Autocomplete <Autocomplete
multiple multiple
size="small" size="small"
options={sspList} disabled={!formData.orgUnitType}
options={filteredOrgUnits}
disableCloseOnSelect disableCloseOnSelect
noOptionsText="Не найдено" noOptionsText="Не найдено"
getOptionLabel={(option) => option.title || ''} getOptionLabel={(option) => option.title || ''}
value={sspList.filter((ssp) => formData.sspIds.includes(ssp.id))} isOptionEqualToValue={(option, value) => option.id === value.id}
renderValue={renderOrgUnitValue}
value={filteredOrgUnits.filter((ssp) =>
formData.org_unit_id.includes(ssp.id),
)}
onChange={(event, newValue) => { onChange={(event, newValue) => {
handleFormChange( handleFormChange(
'sspIds', 'org_unit_id',
newValue.map((ssp) => ssp.id), newValue.map((ssp) => ssp.id),
); );
}} }}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
placeholder={formData.sspIds.length === 0 ? 'Не выбрано' : ''} placeholder={formData.org_unit_id.length === 0 ? 'Не выбрано' : ''}
sx={{ sx={{
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
'& fieldset': { '& fieldset': {
@ -427,12 +503,21 @@ export default function TasksPage() {
}} }}
/> />
)} )}
slots={{
paper: OrgUnitsAutocompletePaper,
}}
slotProps={{ slotProps={{
chip: { paper: {
size: 'small', onSelectAll: handleSelectAllOrgUnits,
isAllSelected: isAllOrgUnitsSelected,
isIndeterminate: isOrgUnitsIndeterminate,
showSelectAll: filteredOrgUnits.length > 0,
},
popper: {
style: { zIndex: 1301 },
}, },
}} }}
/> */} />
</FormControl> </FormControl>
</div> </div>
</Modal> </Modal>

View File

@ -0,0 +1,38 @@
import { Chip, Tooltip } from '@mui/material';
export const ORG_UNIT_TAGS_LIMIT = 2;
export const renderOrgUnitValue = (value, getItemProps) => {
const visible = value.slice(0, ORG_UNIT_TAGS_LIMIT);
const hidden = value.slice(ORG_UNIT_TAGS_LIMIT);
const tags = visible.map((option, index) => {
const { key, ...itemProps } = getItemProps({ index });
return (
<Chip
key={key}
label={option.title || ''}
size="small"
{...itemProps}
/>
);
});
if (hidden.length > 0) {
tags.push(
<Tooltip
key="org-unit-more-tags"
title={hidden.map((option) => option.title).join(', ')}
arrow
>
<Chip
label={`+${hidden.length}`}
size="small"
className="MuiAutocomplete-tag"
/>
</Tooltip>,
);
}
return tags;
};