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);
},
create(payload) {
return api.put("/tasks/", payload).then((r) => r.data);
return api.post("/form/", payload).then((r) => r.data);
},
editTask(taskId, payload = {}) {
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 { useAuth } from './context/AuthProvider';
import { Layout } from './layout/Layout';
import TasksPage from '../pages/TasksPage';
import TasksPage from '../pages/TasksPage/TasksPage';
import FormsPage from '../pages/FormsPage';
import TaskPage from '../pages/TaskPage/TaskPage';
import FormPage from '../pages/FormPage';
@ -39,7 +39,6 @@ export const AppRoutes = () => {
<Route path="/tasks" element={<TasksPage />} />
<Route path="/forms" element={<FormsPage />} />
<Route path="/task/:taskId" element={<TaskPage />} />
<Route path="/form/:formId" element={<FormPage />} />
<Route path="/table/:tableId" element={<TablePage />} />
<Route path="/table-temp/:tableId" element={<TableTempPage />} />
<Route path="/admin_panel/users" element={<UsersPage />} />

View File

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

View File

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