527 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
</>
);
}