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({
}),
}}
/>
-
+ {isAdmin && (
+
+ )}
+
+
+
+ >
+ }>
+ {confirmation?.text}
+
);
}