удаление задач

This commit is contained in:
PotapovaA 2026-09-28 19:12:03 +03:00
parent 056d00b2b4
commit 253e5d7c82
3 changed files with 180 additions and 42 deletions

View File

@ -2,9 +2,10 @@ import { Box } from '@mui/material';
import { FORM_TYPE_TRANSLATE } from '../../constants/constants';
import { DEFAULT_FORM_STYLE, FORM_STYLES } from '../../constants/formStyles';
export default function FormBadge({ formType, label }) {
export default function FormBadge({ formType, label, muted = false }) {
if (!formType && !label) return '—';
const style = FORM_STYLES[formType] || DEFAULT_FORM_STYLE;
const badgeStyle = muted ? { border: '#DDE2EA', color: '#526078', background: '#F3F5F8' } : style;
return (
<Box
component='span'
@ -17,9 +18,9 @@ export default function FormBadge({ formType, label }) {
py: '0.25rem',
px: '0.65rem',
borderRadius: '0.35rem',
border: `1px solid ${style.border}`,
color: style.color,
backgroundColor: style.background,
border: `1px solid ${badgeStyle.border}`,
color: badgeStyle.color,
backgroundColor: badgeStyle.background,
fontSize: '0.75rem',
fontWeight: 700,
lineHeight: 1.4,

View File

@ -4,6 +4,7 @@ import { useSearchParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { SspApi } from '../../api/ssp';
import { TasksApi } from '../../api/tasks';
import { UsersApi } from '../../api/users';
import { useAuth } from '../../app/context/AuthProvider';
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
@ -11,10 +12,10 @@ import { ResetFiltersButton } from '../../components/common/Buttons/ButtonsActio
import Modal from '../../components/common/Modal/Modal';
import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper';
import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue';
import { PageContainer } from '../../components/common/PageContainer';
import { WhitePlus } from '../../components/common/icons/icons';
import { FORM_TYPE_OPTIONS, FORM_TYPE_TRANSLATE, ORG_UNIT_TYPE_OPTIONS, ROLES_NAME_ID } from '../../constants/constants';
import { findItemPage, positiveInteger, readListVisit, rememberListVisit } from '../../utils/listNavigation';
import { PageContainer } from '../../components/common/PageContainer';
import TasksTable from './TasksTable';
const modalSelectMenuProps = {
@ -65,6 +66,7 @@ export default function TasksPage() {
const [totalCount, setTotalCount] = useState(0);
const [sspList, setSspList] = useState([]);
const [usersById, setUsersById] = useState({});
const [formData, setFormData] = useState({
year: '',
orgUnitType: '',
@ -247,6 +249,13 @@ export default function TasksPage() {
}
})
.catch(() => toast.error('Ошибка загрузки подразделений'));
UsersApi.getAll()
.then((data) => {
const users = data.result || [];
setUsersById(Object.fromEntries(users.map((item) => [item.id, item])));
})
.catch(() => toast.error('Ошибка загрузки пользователей'));
}, [user]);
useEffect(() => {
@ -288,6 +297,10 @@ export default function TasksPage() {
onPageChange={handlePageChange}
onLimitChange={handleLimitChange}
lastOpenedId={lastVisit?.id}
usersById={usersById}
isAdmin={user.role_id === ROLES_NAME_ID.admin}
currentUserId={user.id}
onChanged={() => loadTasks(page, limit)}
extraActions={
user.role_id === ROLES_NAME_ID.admin && (
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />} sx={{ height: '2.5rem' }}>

View File

@ -1,15 +1,17 @@
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { Box, Checkbox, IconButton, Link, Menu, MenuItem, Tooltip, Typography } from '@mui/material';
import { Box, Button, Checkbox, IconButton, Link, Menu, MenuItem, Tooltip, Typography } from '@mui/material';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { TasksApi } from '../../api/tasks';
import { ExportButton, ExportExitButton, ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
import FormBadge from '../../components/common/FormBadge';
import { LastOpenedBadge } from '../../components/common/LastOpenedBadge';
import Modal from '../../components/common/Modal/Modal';
import StyledTable from '../../components/common/StyledTable/StyledTable';
import { FORM_TYPE_TRANSLATE } from '../../constants/constants';
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
import { rememberListVisit } from '../../utils/listNavigation';
import StyledTable from '../../components/common/StyledTable/StyledTable';
const actionSx = { height: '2.5rem', whiteSpace: 'nowrap' };
@ -22,6 +24,10 @@ export default function TasksTable({
onPageChange,
onLimitChange,
lastOpenedId,
usersById,
isAdmin,
currentUserId,
onChanged,
extraActions,
filterActions,
}) {
@ -31,6 +37,8 @@ export default function TasksTable({
const [selectedIds, setSelectedIds] = useState([]);
const [exporting, setExporting] = useState(false);
const [actionMenu, setActionMenu] = useState(null);
const [confirmAction, setConfirmAction] = useState(null);
const [actionLoading, setActionLoading] = useState(false);
const lastOpenedRef = useRef(null);
useEffect(() => {
@ -46,7 +54,7 @@ export default function TasksTable({
header: 'Форма',
size: 240,
accessorKey: 'form_type_code',
Cell: ({ cell }) => <FormBadge formType={cell.getValue()} />,
Cell: ({ cell, row }) => <FormBadge formType={cell.getValue()} muted={row.original.to_delete} />,
},
{
accessorKey: 'year',
@ -84,9 +92,29 @@ export default function TasksTable({
</Box>
),
},
{ id: 'administrator', header: 'Администратор', size: 220, Cell: () => <FormBadge label='Админ Админ Админ' /> },
{
accessorKey: 'created_by',
header: 'Администратор',
size: 320,
Cell: ({ cell, row }) => {
const createdBy = cell.getValue();
if (!createdBy) return '—';
const creator = usersById[createdBy];
const creatorName = creator?.full_name?.trim() || creator?.username || `Пользователь #${createdBy}`;
return (
<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 1 }}>
<FormBadge label={creatorName} muted={row.original.to_delete} />
{creator?.is_active === false && (
<Typography component='span' variant='caption' sx={{ color: 'text.disabled', whiteSpace: 'nowrap' }}>
Отправлен на удаление
</Typography>
)}
</Box>
);
},
},
].map((column) => ({ ...column, enableColumnActions: false })),
[exportMode, lastOpenedId, location.pathname, location.search],
[exportMode, lastOpenedId, location.pathname, location.search, usersById],
);
const toggleSelection = (id) => {
@ -117,6 +145,63 @@ export default function TasksTable({
}
};
const openConfirmAction = (type) => {
const task = items.find((item) => item.id === actionMenu?.taskId);
setActionMenu(null);
if (task) setConfirmAction({ type, task });
};
const closeConfirmAction = () => {
if (!actionLoading) setConfirmAction(null);
};
const runConfirmedAction = async () => {
if (!confirmAction) return;
const { type, task } = confirmAction;
try {
setActionLoading(true);
if (type === 'mark') {
await TasksApi.markForDeletion(task.id);
toast.success('Задача помечена на удаление');
}
if (type === 'restore') {
await TasksApi.restore(task.id);
toast.success('Задача возвращена');
}
if (type === 'hardDelete') {
await TasksApi.hardDelete(task.id);
toast.success('Задача полностью удалена');
}
setConfirmAction(null);
await onChanged?.();
} catch (error) {
toast.error(error?.response?.data?.detail || error?.response?.data?.message || 'Не удалось выполнить действие');
} finally {
setActionLoading(false);
}
};
const confirmation = {
mark: {
title: 'Отправить задачу на удаление',
text: 'Задача будет скрыта от пользователей. Администратор сможет вернуть её или удалить окончательно.',
button: 'Отправить',
},
restore: {
title: 'Вернуть задачу',
text: 'Задача снова станет доступна пользователям.',
button: 'Вернуть',
},
hardDelete: {
title: 'Удалить задачу навсегда',
text: 'Будут удалены задача и все связанные данные. Это действие нельзя отменить.',
button: 'Удалить навсегда',
},
}[confirmAction?.type];
const menuTask = items.find((item) => item.id === actionMenu?.taskId);
const canHardDelete = menuTask?.to_delete && String(menuTask.deleted_by) !== String(currentUserId);
return (
<Box sx={{ flex: 1, minHeight: 0, minWidth: 0, width: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', flexShrink: 0, gap: 2, mb: 2 }}>
@ -199,20 +284,22 @@ export default function TasksTable({
}}
/>
</Tooltip>
<Tooltip title='Действия с задачей'>
<IconButton
size='small'
aria-label={`Действия с задачей ${row.original.id}`}
aria-haspopup='menu'
aria-expanded={actionMenu?.taskId === row.original.id ? true : undefined}
aria-controls={actionMenu?.taskId === row.original.id ? 'task-actions-menu' : undefined}
onClick={(event) => {
event.stopPropagation();
setActionMenu({ anchorEl: event.currentTarget, taskId: row.original.id });
}}>
<MoreVertIcon fontSize='small' />
</IconButton>
</Tooltip>
{isAdmin && (
<Tooltip title='Действия с задачей'>
<IconButton
size='small'
aria-label={`Действия с задачей ${row.original.id}`}
aria-haspopup='menu'
aria-expanded={actionMenu?.taskId === row.original.id ? true : undefined}
aria-controls={actionMenu?.taskId === row.original.id ? 'task-actions-menu' : undefined}
onClick={(event) => {
event.stopPropagation();
setActionMenu({ anchorEl: event.currentTarget, taskId: row.original.id });
}}>
<MoreVertIcon fontSize='small' />
</IconButton>
</Tooltip>
)}
</Box>
)}
tableOptions={{
@ -239,10 +326,16 @@ export default function TasksTable({
selected: exportMode && selectedIds.includes(row.original.id),
sx: {
cursor: loading ? 'default' : 'pointer',
'&:nth-of-type(odd):not(.Mui-selected)': { backgroundColor: '#FFFFFF' },
'&:nth-of-type(even):not(.Mui-selected)': { backgroundColor: '#F9FAFB' },
'&:nth-of-type(odd):not(.Mui-selected) > td': {
backgroundColor: row.original.to_delete ? '#F3F5F8' : '#FFFFFF',
color: row.original.to_delete ? '#667085' : 'inherit',
},
'&:nth-of-type(even):not(.Mui-selected) > td': {
backgroundColor: row.original.to_delete ? '#F3F5F8' : '#FFFFFF',
color: row.original.to_delete ? '#667085' : 'inherit',
},
'& > td': { transition: 'background-color 150ms ease' },
'&:hover > td, &:focus-visible > td': { backgroundColor: '#EFF9F0' },
'&:hover > td, &:focus-visible > td': { backgroundColor: row.original.to_delete ? '#EAECF0' : '#EFF9F0' },
'&.Mui-selected > td': { backgroundColor: '#E3F3E5' },
'&.Mui-selected:hover > td, &.Mui-selected:focus-visible > td': { backgroundColor: '#D8EEDC' },
'& > td::after': { backgroundColor: 'transparent !important' },
@ -250,21 +343,52 @@ export default function TasksTable({
}),
}}
/>
<Menu
id='task-actions-menu'
anchorEl={actionMenu?.anchorEl}
open={Boolean(actionMenu)}
onClose={() => setActionMenu(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
<MenuItem
onClick={() => {
setActionMenu(null);
toast.info('Отправка на удаление пока недоступна');
}}>
Отправить на удаление
</MenuItem>
</Menu>
{isAdmin && (
<Menu
id='task-actions-menu'
anchorEl={actionMenu?.anchorEl}
open={Boolean(actionMenu)}
onClose={() => setActionMenu(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
{menuTask?.to_delete ? (
<>
<MenuItem onClick={() => openConfirmAction('restore')}>Вернуть задачу</MenuItem>
<MenuItem disabled={!canHardDelete} onClick={() => openConfirmAction('hardDelete')} sx={{ color: 'error.main' }}>
<Box>
<div>Удалить навсегда</div>
{!canHardDelete && <Typography variant='caption'>Окончательно удалить должен другой администратор</Typography>}
</Box>
</MenuItem>
</>
) : (
<MenuItem onClick={() => openConfirmAction('mark')} sx={{ color: 'error.main' }}>
Отправить на удаление
</MenuItem>
)}
</Menu>
)}
<Modal
open={Boolean(confirmAction)}
onClose={closeConfirmAction}
title={confirmation?.title}
size='small'
actions={
<>
<Button variant='grey' onClick={closeConfirmAction} disabled={actionLoading}>
Отменить
</Button>
<Button
variant='contained'
color={confirmAction?.type === 'restore' ? 'primary' : 'error'}
onClick={runConfirmedAction}
disabled={actionLoading}>
{confirmation?.button}
</Button>
</>
}>
{confirmation?.text}
</Modal>
</Box>
);
}