diff --git a/web/src/components/common/FormBadge.jsx b/web/src/components/common/FormBadge.jsx index ec97918..48e685b 100644 --- a/web/src/components/common/FormBadge.jsx +++ b/web/src/components/common/FormBadge.jsx @@ -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 ( 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 && ( } sx={{ height: '2.5rem' }}> diff --git a/web/src/pages/TasksPage/TasksTable.jsx b/web/src/pages/TasksPage/TasksTable.jsx index 20d92c7..52ea827 100644 --- a/web/src/pages/TasksPage/TasksTable.jsx +++ b/web/src/pages/TasksPage/TasksTable.jsx @@ -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 }) => , + Cell: ({ cell, row }) => , }, { accessorKey: 'year', @@ -84,9 +92,29 @@ export default function TasksTable({ ), }, - { id: 'administrator', header: 'Администратор', size: 220, Cell: () => }, + { + 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 ( + + + {creator?.is_active === false && ( + + Отправлен на удаление + + )} + + ); + }, + }, ].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 ( @@ -199,20 +284,22 @@ export default function TasksTable({ }} /> - - { - event.stopPropagation(); - setActionMenu({ anchorEl: event.currentTarget, taskId: row.original.id }); - }}> - - - + {isAdmin && ( + + { + event.stopPropagation(); + setActionMenu({ anchorEl: event.currentTarget, taskId: row.original.id }); + }}> + + + + )} )} 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({ }), }} /> - setActionMenu(null)} - anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} - transformOrigin={{ vertical: 'top', horizontal: 'right' }}> - { - setActionMenu(null); - toast.info('Отправка на удаление пока недоступна'); - }}> - Отправить на удаление - - + {isAdmin && ( + setActionMenu(null)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }}> + {menuTask?.to_delete ? ( + <> + openConfirmAction('restore')}>Вернуть задачу + openConfirmAction('hardDelete')} sx={{ color: 'error.main' }}> + +
Удалить навсегда
+ {!canHardDelete && Окончательно удалить должен другой администратор} +
+
+ + ) : ( + openConfirmAction('mark')} sx={{ color: 'error.main' }}> + Отправить на удаление + + )} +
+ )} + + + + + }> + {confirmation?.text} +
); }