527 lines
16 KiB
JavaScript
527 lines
16 KiB
JavaScript
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,
|
||
Button,
|
||
TextField,
|
||
FormLabel,
|
||
Autocomplete,
|
||
Stack,
|
||
Pagination,
|
||
PaginationItem,
|
||
Select,
|
||
MenuItem,
|
||
Typography,
|
||
} from '@mui/material';
|
||
import { toast } from 'react-toastify';
|
||
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';
|
||
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([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [query, setQuery] = useState('');
|
||
|
||
// Pagination state
|
||
const [page, setPage] = useState(1);
|
||
const [limit, setLimit] = useState(20);
|
||
const [totalCount, setTotalCount] = useState(0);
|
||
const [isLoadAll, setisLoadAll] = useState(false);
|
||
|
||
const [sspList, setSspList] = useState([]);
|
||
const [formData, setFormData] = useState({
|
||
year: '',
|
||
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);
|
||
const [exportList, setExportList] = useState([]);
|
||
const openModal = () => {
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
if (
|
||
!formData.year ||
|
||
!formData.orgUnitType ||
|
||
formData.org_unit_id.length === 0 ||
|
||
!formData.formTypeCode
|
||
) {
|
||
alert('Пожалуйста, заполните все поля');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoadingUpdate(true);
|
||
|
||
await TasksApi.create({
|
||
year: Number(formData.year),
|
||
form_type_code: formData.formTypeCode,
|
||
org_unit_ids: formData.org_unit_id,
|
||
});
|
||
|
||
setPage(1);
|
||
loadTasks(1, limit);
|
||
|
||
setModalOpen(false);
|
||
setFormData({
|
||
year: '',
|
||
orgUnitType: '',
|
||
org_unit_id: [],
|
||
formTypeCode: '',
|
||
});
|
||
} catch (error) {
|
||
toast.error(
|
||
error.response?.data?.detail ||
|
||
error.response?.data?.message ||
|
||
'Ошибка при создании задачи',
|
||
);
|
||
} finally {
|
||
setLoadingUpdate(false);
|
||
}
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
setModalOpen(false);
|
||
setFormData({
|
||
year: '',
|
||
orgUnitType: '',
|
||
org_unit_id: [],
|
||
formTypeCode: '',
|
||
});
|
||
};
|
||
|
||
const handleFormChange = (field, value) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
[field]: value,
|
||
}));
|
||
};
|
||
|
||
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);
|
||
try {
|
||
const skip = (currentPage - 1) * currentLimit;
|
||
const params = {
|
||
offset: skip,
|
||
limit: currentLimit,
|
||
};
|
||
|
||
const res = await TasksApi.list(params);
|
||
const data = res.result;
|
||
|
||
|
||
setItems(data);
|
||
setTotalCount(res.count);
|
||
|
||
} catch (error) {
|
||
toast.error('Ошибка загрузки задач');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
setPage(1);
|
||
}, [query]);
|
||
|
||
const handlePageChange = (event, value) => {
|
||
setPage(value);
|
||
loadTasks(value, limit);
|
||
};
|
||
|
||
const handleLimitChange = (event) => {
|
||
const newLimit = event.target.value;
|
||
setLimit(newLimit);
|
||
setPage(1);
|
||
loadTasks(1, newLimit);
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (modalOpen) {
|
||
SspApi.getAll({ is_active: true }).then((data) => {
|
||
if (data.result) {
|
||
setSspList(data.result);
|
||
}
|
||
});
|
||
}
|
||
}, [modalOpen]);
|
||
|
||
useEffect(() => {
|
||
loadTasks(page, limit);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
setExportList([]);
|
||
}, [exportMode]);
|
||
|
||
const handleChangeToExportList = (taskId, checked) => {
|
||
if (checked) {
|
||
setExportList((prev) => [...prev, taskId]);
|
||
} else {
|
||
const newExportList = exportList.filter((t) => t != taskId);
|
||
setExportList(newExportList);
|
||
}
|
||
};
|
||
|
||
const handleBulkExport = async () => {
|
||
await exportBulkForms(exportList);
|
||
setExportMode(false);
|
||
};
|
||
|
||
const handleSingleExport = (taskId, taskTitle) => {
|
||
exportSingleForm(taskId, taskTitle);
|
||
};
|
||
|
||
// Calculate total pages
|
||
const totalPages = Math.ceil(totalCount / limit);
|
||
|
||
if (!user) return;
|
||
|
||
return (
|
||
<>
|
||
<PageContainer>
|
||
<HeaderSwitch />
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||
{exportMode && <span>Выделено задач: {exportList.length}</span>}
|
||
</Box>
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||
<div style={{ width: '20rem' }}>
|
||
{/* <SearchComponent
|
||
placeholder="Поиск по названию задачи"
|
||
value={query}
|
||
onChange={setQuery}
|
||
/> */}
|
||
</div>
|
||
<Stack direction={'row'} spacing={'.5rem'}>
|
||
{!exportMode ? (
|
||
<>
|
||
<ExportWithTextButton
|
||
text={'Пакетный экспорт'}
|
||
onClick={() => {
|
||
setExportMode(true);
|
||
}}
|
||
></ExportWithTextButton>
|
||
{user.role_id == ROLES_NAME_ID.admin && (
|
||
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />}>
|
||
Создать задачу
|
||
</PrimaryButton>
|
||
)}
|
||
</>
|
||
) : (
|
||
<>
|
||
<ExportWithTextButton
|
||
text={'Экспортировать'}
|
||
onClick={handleBulkExport}
|
||
disabled={exportList.length === 0}
|
||
/>
|
||
<ExportExitButton onClick={() => setExportMode(false)} />
|
||
</>
|
||
)}
|
||
</Stack>
|
||
</Box>
|
||
|
||
{/* TODO: вернуть, когда будет реализовано на бэке */}
|
||
{/* <FilterComponent statusFilter={statusFilter} setStatusFilter={setStatusFilter} /> */}
|
||
<TasksContainer>
|
||
{loading ? (
|
||
<CardsSkeleton count={limit} />
|
||
) : (
|
||
<>
|
||
{items.map((t) => (
|
||
<TaskCardComponent
|
||
exportMode={exportMode}
|
||
taskForm={t}
|
||
key={t.id + '-task-id'}
|
||
onAddForExport={(checked) => {
|
||
handleChangeToExportList(t.id, checked);
|
||
}}
|
||
onExportClick={handleSingleExport}
|
||
/>
|
||
))}
|
||
</>
|
||
)}
|
||
</TasksContainer>
|
||
|
||
{/* Pagination Footer */}
|
||
{!loading && items.length > 0 && (
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
pt: 2,
|
||
pb: 2,
|
||
borderTop: '1px solid',
|
||
borderColor: 'divider',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
<Typography variant="body2" color="textSecondary">
|
||
Показать на странице:
|
||
</Typography>
|
||
<Select
|
||
value={limit}
|
||
onChange={handleLimitChange}
|
||
size="small"
|
||
sx={{ minWidth: 70 }}
|
||
disabled={loading}
|
||
>
|
||
<MenuItem value={10}>10</MenuItem>
|
||
<MenuItem value={20}>20</MenuItem>
|
||
<MenuItem value={50}>50</MenuItem>
|
||
<MenuItem value={100}>100</MenuItem>
|
||
<MenuItem value={200}>200</MenuItem>
|
||
</Select>
|
||
</Box>
|
||
</Box>
|
||
<Pagination
|
||
count={Math.ceil(totalCount / limit)}
|
||
page={page}
|
||
onChange={handlePageChange}
|
||
color="primary"
|
||
size="large"
|
||
showFirstButton
|
||
showLastButton
|
||
disabled={loading}
|
||
renderItem={(item) => (
|
||
<PaginationItem
|
||
{...item}
|
||
sx={{
|
||
'&.Mui-selected': {
|
||
backgroundColor: 'primary.main',
|
||
color: 'white',
|
||
'&:hover': {
|
||
backgroundColor: 'primary.dark',
|
||
},
|
||
},
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
<Box sx={{ width: 120 }} /> {/* Spacer for symmetry */}
|
||
</Box>
|
||
)}
|
||
</PageContainer>
|
||
<Modal
|
||
open={modalOpen}
|
||
onClose={handleCancel}
|
||
title="Создать задачу"
|
||
actions={
|
||
<>
|
||
<Button
|
||
variant="grey"
|
||
onClick={handleCancel}
|
||
style={{ height: '2.5rem' }}
|
||
disabled={loadingUpdate}
|
||
>
|
||
Отменить
|
||
</Button>
|
||
<Button
|
||
variant="contained"
|
||
onClick={handleSubmit}
|
||
style={{ height: '2.5rem' }}
|
||
loading={loadingUpdate}
|
||
>
|
||
Создать
|
||
</Button>
|
||
</>
|
||
}
|
||
>
|
||
<div className="space-y-4">
|
||
<FormControl fullWidth size="small">
|
||
<FormLabel required>Форма</FormLabel>
|
||
<Select
|
||
size="small"
|
||
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>
|
||
<FormLabel required>Год</FormLabel>
|
||
<TextField
|
||
size="small"
|
||
type="number"
|
||
placeholder="Укажите год выполнения задачи"
|
||
value={formData.year}
|
||
onChange={(e) => handleFormChange('year', e.target.value)}
|
||
/>
|
||
</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
|
||
multiple
|
||
size="small"
|
||
disabled={!formData.orgUnitType}
|
||
options={filteredOrgUnits}
|
||
disableCloseOnSelect
|
||
noOptionsText="Не найдено"
|
||
getOptionLabel={(option) => option.title || ''}
|
||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||
renderValue={renderOrgUnitValue}
|
||
value={filteredOrgUnits.filter((ssp) =>
|
||
formData.org_unit_id.includes(ssp.id),
|
||
)}
|
||
onChange={(event, newValue) => {
|
||
handleFormChange(
|
||
'org_unit_id',
|
||
newValue.map((ssp) => ssp.id),
|
||
);
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField
|
||
{...params}
|
||
placeholder={formData.org_unit_id.length === 0 ? 'Не выбрано' : ''}
|
||
sx={{
|
||
'& .MuiOutlinedInput-root': {
|
||
'& fieldset': {
|
||
borderColor: 'rgba(0,0,0,0.1)',
|
||
},
|
||
},
|
||
}}
|
||
/>
|
||
)}
|
||
slots={{
|
||
paper: OrgUnitsAutocompletePaper,
|
||
}}
|
||
slotProps={{
|
||
paper: {
|
||
onSelectAll: handleSelectAllOrgUnits,
|
||
isAllSelected: isAllOrgUnitsSelected,
|
||
isIndeterminate: isOrgUnitsIndeterminate,
|
||
showSelectAll: filteredOrgUnits.length > 0,
|
||
},
|
||
popper: {
|
||
style: { zIndex: 1301 },
|
||
},
|
||
}}
|
||
/>
|
||
</FormControl>
|
||
</div>
|
||
</Modal>
|
||
</>
|
||
);
|
||
}
|