diff --git a/web/src/api/tasks.js b/web/src/api/tasks.js index db7291d..8d1016a 100644 --- a/web/src/api/tasks.js +++ b/web/src/api/tasks.js @@ -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); diff --git a/web/src/app/Routes.jsx b/web/src/app/Routes.jsx index 4c26684..2069477 100644 --- a/web/src/app/Routes.jsx +++ b/web/src/app/Routes.jsx @@ -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 = () => { } /> } /> } /> - } /> } /> } /> } /> diff --git a/web/src/components/HeaderMenu/HeaderSwitch.jsx b/web/src/components/HeaderMenu/HeaderSwitch.jsx index d26d3fa..dcd49f1 100644 --- a/web/src/components/HeaderMenu/HeaderSwitch.jsx +++ b/web/src/components/HeaderMenu/HeaderSwitch.jsx @@ -75,7 +75,7 @@ export function HeaderSwitch() { } > - Задачи и формы + Задачи {user.role_id === ROLES_NAME_ID.admin && ( + {showSelectAll && ( + event.preventDefault()} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 0.5, + px: 2, + py: 1, + borderBottom: '1px solid', + borderColor: 'divider', + }} + > + onSelectAll(event.target.checked)} + /> + + Выделить все + + + )} + {children} + + ); + }, +); diff --git a/web/src/pages/TasksPage.jsx b/web/src/pages/TasksPage/TasksPage.jsx similarity index 63% rename from web/src/pages/TasksPage.jsx rename to web/src/pages/TasksPage/TasksPage.jsx index f007b84..2602436 100644 --- a/web/src/pages/TasksPage.jsx +++ b/web/src/pages/TasksPage/TasksPage.jsx @@ -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,44 @@ 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 Promise.all( + formData.org_unit_id.map((orgUnitId) => + TasksApi.create({ + year: Number(formData.year), + form_type_code: formData.formTypeCode, + org_unit_id: orgUnitId, + }), + ), + ); - // 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 +143,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 +158,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 +220,6 @@ export default function TasksPage() { setSspList(data.result); } }); - FormsApi.formsList().then((data) => { - if (data) { - setForms(data); - } - }); } }, [modalOpen]); @@ -350,41 +407,30 @@ export default function TasksPage() { } >
- - Название задачи - {/* handleFormChange('title', e.target.value)} - /> */} - - Форма - 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) => ( - - )} - /> + displayEmpty + fullWidth + value={formData.formTypeCode} + onChange={(e) => handleFormChange('formTypeCode', e.target.value)} + renderValue={(selected) => + selected ? ( + selected + ) : ( + Не выбрана + ) + } + MenuProps={modalSelectMenuProps} + sx={modalSelectSx} + > + {FORM_TYPE_OPTIONS.map((formType) => ( + + {formType} + + ))} + @@ -398,26 +444,60 @@ export default function TasksPage() { /> + + Тип + + + ССП/Региональный филиал - {/* 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) => ( )} + slots={{ + paper: OrgUnitsAutocompletePaper, + }} slotProps={{ - chip: { - size: 'small', + paper: { + onSelectAll: handleSelectAllOrgUnits, + isAllSelected: isAllOrgUnitsSelected, + isIndeterminate: isOrgUnitsIndeterminate, + showSelectAll: filteredOrgUnits.length > 0, + }, + popper: { + style: { zIndex: 1301 }, }, }} - /> */} + />
diff --git a/web/src/pages/TasksPage/renderOrgUnitValue.jsx b/web/src/pages/TasksPage/renderOrgUnitValue.jsx new file mode 100644 index 0000000..998f472 --- /dev/null +++ b/web/src/pages/TasksPage/renderOrgUnitValue.jsx @@ -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 ( + + ); + }); + + if (hidden.length > 0) { + tags.push( + option.title).join(', ')} + arrow + > + + , + ); + } + + return tags; +};