diff --git a/web/src/components/common/DeletionMarkChip.jsx b/web/src/components/common/DeletionMarkChip.jsx new file mode 100644 index 0000000..289640e --- /dev/null +++ b/web/src/components/common/DeletionMarkChip.jsx @@ -0,0 +1,11 @@ +import { Tooltip } from '@mui/material'; +import { formatDeletionHint } from '../../utils/formatDeletionHint'; +import { Chip } from '../styles/StyledChip'; + +const chipSx = { background: '#fde8e8', color: '#b42318', whiteSpace: 'nowrap', width: 'max-content', cursor: 'default' }; + +export const DeletionMarkChip = ({ task, usersById, sx = {} }) => ( + + На удаление + +); diff --git a/web/src/components/common/TaskCardComponent.jsx b/web/src/components/common/TaskCardComponent.jsx index 07cde07..128f9c1 100644 --- a/web/src/components/common/TaskCardComponent.jsx +++ b/web/src/components/common/TaskCardComponent.jsx @@ -9,6 +9,7 @@ import { TasksApi } from '../../api/tasks'; import { FORM_TYPE_TRANSLATE } from '../../constants/constants'; import { ExportButton } from '../common/Buttons/ButtonsActions'; import { Chip } from '../styles/StyledChip'; +import { DeletionMarkChip } from './DeletionMarkChip'; import { LastOpenedBadge } from './LastOpenedBadge'; import { IconWithContent } from './IconWithContent'; import Modal from './Modal/Modal'; @@ -62,6 +63,7 @@ export const TaskCardComponent = ({ isAdmin = false, onChanged, currentUserId = null, + usersById = {}, }) => { const navigate = useNavigate(); const location = useLocation(); @@ -213,7 +215,7 @@ export const TaskCardComponent = ({ ID: {id} - {taskForm.to_delete && На удаление} + {taskForm.to_delete && } {isLastOpened && Последняя открытая} diff --git a/web/src/pages/TaskPage/TaskPage.jsx b/web/src/pages/TaskPage/TaskPage.jsx index 91d37e3..fd77b74 100644 --- a/web/src/pages/TaskPage/TaskPage.jsx +++ b/web/src/pages/TaskPage/TaskPage.jsx @@ -4,9 +4,11 @@ import { useNavigate, useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import { ProjectsApi } from '../../api/projects'; // Добавьте импорт API для проектов import { TasksApi } from '../../api/tasks'; +import { UsersApi } from '../../api/users'; import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal'; import { DangerOutlinedButton, PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons'; import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; +import { DeletionMarkChip } from '../../components/common/DeletionMarkChip'; import { IconWithContent } from '../../components/common/IconWithContent'; import { HeaderPortal } from '../../components/common/HeaderPortal'; import SearchComponent from '../../components/common/SearchComponent'; @@ -49,6 +51,15 @@ export default function TaskPage() { const [addYearModalOpen, setAddYearModalOpen] = useState(false); const [isAddingYear, setIsAddingYear] = useState(false); const [dataReloadKey, setDataReloadKey] = useState(0); + const [usersById, setUsersById] = useState({}); + const isMarkedForDeletion = Boolean(task?.to_delete); + + useEffect(() => { + if (!isMarkedForDeletion) return; + UsersApi.getAll() + .then((data) => setUsersById(Object.fromEntries((data.result || []).map((item) => [item.id, item])))) + .catch(() => toast.error('Ошибка загрузки пользователей')); + }, [isMarkedForDeletion]); useEffect(() => { const getData = async () => { @@ -162,6 +173,7 @@ export default function TaskPage() { Таблиц: {tables.length} + {isMarkedForDeletion && } diff --git a/web/src/pages/TasksPage/TasksTable.jsx b/web/src/pages/TasksPage/TasksTable.jsx index 52ea827..fe97cc7 100644 --- a/web/src/pages/TasksPage/TasksTable.jsx +++ b/web/src/pages/TasksPage/TasksTable.jsx @@ -5,6 +5,7 @@ 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 { DeletionMarkChip } from '../../components/common/DeletionMarkChip'; import FormBadge from '../../components/common/FormBadge'; import { LastOpenedBadge } from '../../components/common/LastOpenedBadge'; import Modal from '../../components/common/Modal/Modal'; @@ -84,6 +85,7 @@ export default function TasksTable({ sx={{ color: 'inherit', fontWeight: 500, fontSize: '1rem' }}> {cell.getValue()} + {row.original.to_delete && } {String(row.original.id) === lastOpenedId && ( Последняя открытая @@ -106,7 +108,7 @@ export default function TasksTable({ {creator?.is_active === false && ( - Отправлен на удаление + Пользователь удалён )} diff --git a/web/src/utils/formatDeletionHint.js b/web/src/utils/formatDeletionHint.js new file mode 100644 index 0000000..0583511 --- /dev/null +++ b/web/src/utils/formatDeletionHint.js @@ -0,0 +1,22 @@ +const pad = (n) => String(n).padStart(2, '0'); + +const formatDateTime = (value) => { + if (!value) return ''; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return ''; + return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +}; + +/** Текст подсказки для чипа «На удаление»: когда и кем задача отправлена на удаление. */ +export const formatDeletionHint = (task, usersById) => { + const parts = ['Отправлена на удаление']; + const when = formatDateTime(task?.deleted_at); + if (when) parts[0] += ` ${when}`; + const userId = task?.deleted_by; + if (userId !== null && userId !== undefined && userId !== '') { + const user = usersById?.[userId]; + const name = user?.full_name?.trim() || user?.username || `Пользователь #${userId}`; + parts.push(name); + } + return parts.join(' — '); +}; diff --git a/web/src/utils/formatDeletionHint.test.js b/web/src/utils/formatDeletionHint.test.js new file mode 100644 index 0000000..e77ab62 --- /dev/null +++ b/web/src/utils/formatDeletionHint.test.js @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { formatDeletionHint } from './formatDeletionHint.js'; + +const at = new Date(2026, 10, 5, 9, 7).toISOString(); +const users = { 7: { id: 7, full_name: ' Иванов Иван ', username: 'ivanov' } }; + +test('дата и ФИО', () => { + assert.equal(formatDeletionHint({ deleted_at: at, deleted_by: 7 }, users), 'Отправлена на удаление 05.11.2026 09:07 — Иванов Иван'); +}); + +test('без deleted_by', () => { + assert.equal(formatDeletionHint({ deleted_at: at, deleted_by: null }, users), 'Отправлена на удаление 05.11.2026 09:07'); +}); + +test('без deleted_at', () => { + assert.equal(formatDeletionHint({ deleted_by: 7 }, users), 'Отправлена на удаление — Иванов Иван'); +}); + +test('без даты и пользователя', () => { + assert.equal(formatDeletionHint({}, users), 'Отправлена на удаление'); + assert.equal(formatDeletionHint(null, null), 'Отправлена на удаление'); +}); + +test('deleted_by нет в справочнике', () => { + assert.equal(formatDeletionHint({ deleted_at: at, deleted_by: 99 }, users), 'Отправлена на удаление 05.11.2026 09:07 — Пользователь #99'); + assert.equal(formatDeletionHint({ deleted_by: 99 }, undefined), 'Отправлена на удаление — Пользователь #99'); +}); + +test('нет full_name — username; невалидная дата игнорируется', () => { + assert.equal( + formatDeletionHint({ deleted_at: 'мусор', deleted_by: 8 }, { 8: { username: 'petrov' } }), + 'Отправлена на удаление — petrov', + ); +});