удаление задач
This commit is contained in:
parent
056d00b2b4
commit
253e5d7c82
@ -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,
|
||||
|
||||
@ -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' }}>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user