удаление задач
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 { FORM_TYPE_TRANSLATE } from '../../constants/constants';
|
||||||
import { DEFAULT_FORM_STYLE, FORM_STYLES } from '../../constants/formStyles';
|
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 '—';
|
if (!formType && !label) return '—';
|
||||||
const style = FORM_STYLES[formType] || DEFAULT_FORM_STYLE;
|
const style = FORM_STYLES[formType] || DEFAULT_FORM_STYLE;
|
||||||
|
const badgeStyle = muted ? { border: '#DDE2EA', color: '#526078', background: '#F3F5F8' } : style;
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component='span'
|
component='span'
|
||||||
@ -17,9 +18,9 @@ export default function FormBadge({ formType, label }) {
|
|||||||
py: '0.25rem',
|
py: '0.25rem',
|
||||||
px: '0.65rem',
|
px: '0.65rem',
|
||||||
borderRadius: '0.35rem',
|
borderRadius: '0.35rem',
|
||||||
border: `1px solid ${style.border}`,
|
border: `1px solid ${badgeStyle.border}`,
|
||||||
color: style.color,
|
color: badgeStyle.color,
|
||||||
backgroundColor: style.background,
|
backgroundColor: badgeStyle.background,
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { SspApi } from '../../api/ssp';
|
import { SspApi } from '../../api/ssp';
|
||||||
import { TasksApi } from '../../api/tasks';
|
import { TasksApi } from '../../api/tasks';
|
||||||
|
import { UsersApi } from '../../api/users';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
||||||
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
|
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 Modal from '../../components/common/Modal/Modal';
|
||||||
import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper';
|
import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper';
|
||||||
import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue';
|
import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue';
|
||||||
|
import { PageContainer } from '../../components/common/PageContainer';
|
||||||
import { WhitePlus } from '../../components/common/icons/icons';
|
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 { 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 { findItemPage, positiveInteger, readListVisit, rememberListVisit } from '../../utils/listNavigation';
|
||||||
import { PageContainer } from '../../components/common/PageContainer';
|
|
||||||
import TasksTable from './TasksTable';
|
import TasksTable from './TasksTable';
|
||||||
|
|
||||||
const modalSelectMenuProps = {
|
const modalSelectMenuProps = {
|
||||||
@ -65,6 +66,7 @@ export default function TasksPage() {
|
|||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
|
||||||
const [sspList, setSspList] = useState([]);
|
const [sspList, setSspList] = useState([]);
|
||||||
|
const [usersById, setUsersById] = useState({});
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
year: '',
|
year: '',
|
||||||
orgUnitType: '',
|
orgUnitType: '',
|
||||||
@ -247,6 +249,13 @@ export default function TasksPage() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => toast.error('Ошибка загрузки подразделений'));
|
.catch(() => toast.error('Ошибка загрузки подразделений'));
|
||||||
|
|
||||||
|
UsersApi.getAll()
|
||||||
|
.then((data) => {
|
||||||
|
const users = data.result || [];
|
||||||
|
setUsersById(Object.fromEntries(users.map((item) => [item.id, item])));
|
||||||
|
})
|
||||||
|
.catch(() => toast.error('Ошибка загрузки пользователей'));
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -288,6 +297,10 @@ export default function TasksPage() {
|
|||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onLimitChange={handleLimitChange}
|
onLimitChange={handleLimitChange}
|
||||||
lastOpenedId={lastVisit?.id}
|
lastOpenedId={lastVisit?.id}
|
||||||
|
usersById={usersById}
|
||||||
|
isAdmin={user.role_id === ROLES_NAME_ID.admin}
|
||||||
|
currentUserId={user.id}
|
||||||
|
onChanged={() => loadTasks(page, limit)}
|
||||||
extraActions={
|
extraActions={
|
||||||
user.role_id === ROLES_NAME_ID.admin && (
|
user.role_id === ROLES_NAME_ID.admin && (
|
||||||
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />} sx={{ height: '2.5rem' }}>
|
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />} sx={{ height: '2.5rem' }}>
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
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 { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
|
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { TasksApi } from '../../api/tasks';
|
||||||
import { ExportButton, ExportExitButton, ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
|
import { ExportButton, ExportExitButton, ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
|
||||||
import FormBadge from '../../components/common/FormBadge';
|
import FormBadge from '../../components/common/FormBadge';
|
||||||
import { LastOpenedBadge } from '../../components/common/LastOpenedBadge';
|
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 { FORM_TYPE_TRANSLATE } from '../../constants/constants';
|
||||||
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
|
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
|
||||||
import { rememberListVisit } from '../../utils/listNavigation';
|
import { rememberListVisit } from '../../utils/listNavigation';
|
||||||
import StyledTable from '../../components/common/StyledTable/StyledTable';
|
|
||||||
|
|
||||||
const actionSx = { height: '2.5rem', whiteSpace: 'nowrap' };
|
const actionSx = { height: '2.5rem', whiteSpace: 'nowrap' };
|
||||||
|
|
||||||
@ -22,6 +24,10 @@ export default function TasksTable({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
lastOpenedId,
|
lastOpenedId,
|
||||||
|
usersById,
|
||||||
|
isAdmin,
|
||||||
|
currentUserId,
|
||||||
|
onChanged,
|
||||||
extraActions,
|
extraActions,
|
||||||
filterActions,
|
filterActions,
|
||||||
}) {
|
}) {
|
||||||
@ -31,6 +37,8 @@ export default function TasksTable({
|
|||||||
const [selectedIds, setSelectedIds] = useState([]);
|
const [selectedIds, setSelectedIds] = useState([]);
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
const [actionMenu, setActionMenu] = useState(null);
|
const [actionMenu, setActionMenu] = useState(null);
|
||||||
|
const [confirmAction, setConfirmAction] = useState(null);
|
||||||
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
const lastOpenedRef = useRef(null);
|
const lastOpenedRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -46,7 +54,7 @@ export default function TasksTable({
|
|||||||
header: 'Форма',
|
header: 'Форма',
|
||||||
size: 240,
|
size: 240,
|
||||||
accessorKey: 'form_type_code',
|
accessorKey: 'form_type_code',
|
||||||
Cell: ({ cell }) => <FormBadge formType={cell.getValue()} />,
|
Cell: ({ cell, row }) => <FormBadge formType={cell.getValue()} muted={row.original.to_delete} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'year',
|
accessorKey: 'year',
|
||||||
@ -84,9 +92,29 @@ export default function TasksTable({
|
|||||||
</Box>
|
</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 })),
|
].map((column) => ({ ...column, enableColumnActions: false })),
|
||||||
[exportMode, lastOpenedId, location.pathname, location.search],
|
[exportMode, lastOpenedId, location.pathname, location.search, usersById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleSelection = (id) => {
|
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 (
|
return (
|
||||||
<Box sx={{ flex: 1, minHeight: 0, minWidth: 0, width: '100%', display: 'flex', flexDirection: 'column' }}>
|
<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 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', flexShrink: 0, gap: 2, mb: 2 }}>
|
||||||
@ -199,20 +284,22 @@ export default function TasksTable({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title='Действия с задачей'>
|
{isAdmin && (
|
||||||
<IconButton
|
<Tooltip title='Действия с задачей'>
|
||||||
size='small'
|
<IconButton
|
||||||
aria-label={`Действия с задачей ${row.original.id}`}
|
size='small'
|
||||||
aria-haspopup='menu'
|
aria-label={`Действия с задачей ${row.original.id}`}
|
||||||
aria-expanded={actionMenu?.taskId === row.original.id ? true : undefined}
|
aria-haspopup='menu'
|
||||||
aria-controls={actionMenu?.taskId === row.original.id ? 'task-actions-menu' : undefined}
|
aria-expanded={actionMenu?.taskId === row.original.id ? true : undefined}
|
||||||
onClick={(event) => {
|
aria-controls={actionMenu?.taskId === row.original.id ? 'task-actions-menu' : undefined}
|
||||||
event.stopPropagation();
|
onClick={(event) => {
|
||||||
setActionMenu({ anchorEl: event.currentTarget, taskId: row.original.id });
|
event.stopPropagation();
|
||||||
}}>
|
setActionMenu({ anchorEl: event.currentTarget, taskId: row.original.id });
|
||||||
<MoreVertIcon fontSize='small' />
|
}}>
|
||||||
</IconButton>
|
<MoreVertIcon fontSize='small' />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
tableOptions={{
|
tableOptions={{
|
||||||
@ -239,10 +326,16 @@ export default function TasksTable({
|
|||||||
selected: exportMode && selectedIds.includes(row.original.id),
|
selected: exportMode && selectedIds.includes(row.original.id),
|
||||||
sx: {
|
sx: {
|
||||||
cursor: loading ? 'default' : 'pointer',
|
cursor: loading ? 'default' : 'pointer',
|
||||||
'&:nth-of-type(odd):not(.Mui-selected)': { backgroundColor: '#FFFFFF' },
|
'&:nth-of-type(odd):not(.Mui-selected) > td': {
|
||||||
'&:nth-of-type(even):not(.Mui-selected)': { backgroundColor: '#F9FAFB' },
|
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' },
|
'& > 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 > td': { backgroundColor: '#E3F3E5' },
|
||||||
'&.Mui-selected:hover > td, &.Mui-selected:focus-visible > td': { backgroundColor: '#D8EEDC' },
|
'&.Mui-selected:hover > td, &.Mui-selected:focus-visible > td': { backgroundColor: '#D8EEDC' },
|
||||||
'& > td::after': { backgroundColor: 'transparent !important' },
|
'& > td::after': { backgroundColor: 'transparent !important' },
|
||||||
@ -250,21 +343,52 @@ export default function TasksTable({
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Menu
|
{isAdmin && (
|
||||||
id='task-actions-menu'
|
<Menu
|
||||||
anchorEl={actionMenu?.anchorEl}
|
id='task-actions-menu'
|
||||||
open={Boolean(actionMenu)}
|
anchorEl={actionMenu?.anchorEl}
|
||||||
onClose={() => setActionMenu(null)}
|
open={Boolean(actionMenu)}
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
onClose={() => setActionMenu(null)}
|
||||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
<MenuItem
|
transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
|
||||||
onClick={() => {
|
{menuTask?.to_delete ? (
|
||||||
setActionMenu(null);
|
<>
|
||||||
toast.info('Отправка на удаление пока недоступна');
|
<MenuItem onClick={() => openConfirmAction('restore')}>Вернуть задачу</MenuItem>
|
||||||
}}>
|
<MenuItem disabled={!canHardDelete} onClick={() => openConfirmAction('hardDelete')} sx={{ color: 'error.main' }}>
|
||||||
Отправить на удаление
|
<Box>
|
||||||
</MenuItem>
|
<div>Удалить навсегда</div>
|
||||||
</Menu>
|
{!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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user