DFiP_Budget_planing/web/src/components/common/TaskCardComponent.jsx

287 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import styled from '@emotion/styled';
import { Box, Button, FormControl, FormLabel, Stack, TextField } from '@mui/material';
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { rememberListVisit } from '../../utils/listNavigation';
import { toast } from 'react-toastify';
import { FormsApi } from '../../api/forms';
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';
import { CheckBoxCheckSvg, CheckBoxUncheckSvg, DateIcon, TableIcon } from './icons/icons';
import { TaskCard } from '../styles/TaskCard.style';
const CardHeader = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
`;
const TaskId = styled.div`
font-size: 0.75rem;
font-weight: 400;
line-height: 1rem;
letter-spacing: 0rem;
text-align: left;
`;
const CardBody = styled.div`
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: flex-start;
`;
const TaskTitle = styled.div`
font-size: 1rem;
font-weight: 500;
color: var(--Text-Primary, #333);
line-height: 1.4;
margin: 0.375rem 0 0;
padding: 0;
`;
export const TaskCardComponent = ({
taskForm,
isForm = false,
fluidWidth = false,
exportMode = false,
isLoadingFile = false,
onAddForExport,
onExportClick,
isLastOpened = false,
isAdmin = false,
onChanged,
currentUserId = null,
usersById = {},
}) => {
const navigate = useNavigate();
const location = useLocation();
const cardRef = useRef(null);
useEffect(() => {
if (isLastOpened) cardRef.current?.scrollIntoView({ block: 'nearest' });
}, [isLastOpened]);
const { id, title, org_unit, form_type_code, year } = taskForm;
const canHardDelete = taskForm.to_delete && taskForm.deleted_by !== currentUserId;
const [isEditModalOpen, setEditModalOpen] = useState(false);
const [editedTitle, setEditedTitle] = useState('');
const [currentTypeCode, setCurrentTypeCode] = useState(FORM_TYPE_TRANSLATE[form_type_code] || '');
const [isChecked, setIsChecked] = useState(false);
const [confirmAction, setConfirmAction] = useState(null);
const [actionLoading, setActionLoading] = useState(false);
useEffect(() => {
setIsChecked(false);
}, [exportMode]);
const handleOpenEdit = (event) => {
event.stopPropagation();
setEditedTitle(currentTypeCode);
setEditModalOpen(true);
};
const handleCloseEdit = () => {
setEditModalOpen(false);
};
const handleCheckboxClick = (event) => {
event.stopPropagation();
if (!isLoadingFile) {
setIsChecked(!isChecked);
onAddForExport(!isChecked);
} else {
toast.warning('Файл формируется!');
}
};
const handleExportTask = (event) => {
event.stopPropagation();
onExportClick?.(id, title);
};
const openConfirmAction = (event, action) => {
event.stopPropagation();
setConfirmAction(action);
};
const closeConfirmAction = () => {
if (!actionLoading) setConfirmAction(null);
};
const runConfirmedAction = async () => {
if (!confirmAction) return;
try {
setActionLoading(true);
if (confirmAction === 'mark') {
await TasksApi.markForDeletion(id);
toast.success('Задача помечена на удаление');
}
if (confirmAction === 'restore') {
await TasksApi.restore(id);
toast.success('Задача возвращена');
}
if (confirmAction === 'hardDelete') {
await TasksApi.hardDelete(id);
toast.success('Задача полностью удалена');
}
setConfirmAction(null);
onChanged?.();
} catch (error) {
toast.error(error?.response?.data?.detail || 'Не удалось выполнить действие');
} finally {
setActionLoading(false);
}
};
const actionTitle = {
mark: 'Удалить задачу',
restore: 'Вернуть задачу',
hardDelete: 'Полностью удалить задачу',
}[confirmAction];
const actionText = {
mark: 'Задача будет скрыта от пользователей. Админы смогут вернуть её или удалить окончательно.',
restore: 'Задача снова станет доступна пользователям.',
hardDelete: 'Будут удалены задача и все связанные данные. Это действие нельзя отменить.',
}[confirmAction];
const updateTask = async (nextTitle) => {
try {
await TasksApi.editTask(id, { title: nextTitle });
setCurrentTypeCode(nextTitle);
setEditModalOpen(false);
toast.success('Название задачи обновлено');
} catch (error) {
toast.error(error?.response?.data?.detail || 'Не удалось обновить название задачи');
}
};
const updateForm = async (nextTitle) => {
try {
await FormsApi.updateForm(id, { title: nextTitle });
setCurrentTypeCode(nextTitle);
setEditModalOpen(false);
} catch (error) {
toast.error(error?.response?.data?.detail || 'Не удалось обновить название формы');
}
};
const handleSaveTitle = async () => {
const nextTitle = editedTitle.trim();
if (!nextTitle) {
toast.error('Название не может быть пустым');
return;
}
if (nextTitle === currentTypeCode) {
setEditModalOpen(false);
return;
}
if (isForm) {
await updateForm(nextTitle);
return;
}
await updateTask(nextTitle);
};
return (
<>
<TaskCard
ref={cardRef}
$fluidWidth={fluidWidth}
$marked={Boolean(taskForm.to_delete)}
onClick={(event) => {
if (exportMode) {
handleCheckboxClick(event);
} else {
if (!isForm && location.pathname === '/tasks') rememberListVisit('tasks', id, `${location.pathname}${location.search}`);
navigate(`/${isForm ? 'form' : 'task'}/${id}`);
}
}}
key={id}>
<Box sx={{ width: '100%' }}>
<CardHeader>
<TaskId>ID: {id}</TaskId>
<Stack direction='row' gap={0.75} alignItems='center'>
{taskForm.to_delete && <DeletionMarkChip task={taskForm} usersById={usersById} />}
{isLastOpened && <LastOpenedBadge sx={{ mb: 0 }}>Последняя открытая</LastOpenedBadge>}
</Stack>
</CardHeader>
<TaskTitle>{org_unit?.title || 'Подразделение не указано'}</TaskTitle>
</Box>
<Stack
direction='row'
sx={{
marginTop: 'auto',
gap: '0.5rem',
justifyContent: 'space-between',
width: '100%',
alignItems: 'flex-end',
}}>
<CardBody>
<IconWithContent icon={<DateIcon />}>{year}</IconWithContent>
<IconWithContent icon={<TableIcon />}>
<Chip sx={{ background: 'transparent' }}>{currentTypeCode}</Chip>
</IconWithContent>
</CardBody>
<Stack direction='row' gap={0.75} alignItems='center' onClick={(event) => event.stopPropagation()}>
{isAdmin && !isForm && !exportMode && !taskForm.to_delete && (
<Button size='small' variant='text' color='error' onClick={(event) => openConfirmAction(event, 'mark')}>
Удалить
</Button>
)}
{isAdmin && !isForm && !exportMode && taskForm.to_delete && (
<>
<Button size='small' variant='text' onClick={(event) => openConfirmAction(event, 'restore')}>
Вернуть
</Button>
{canHardDelete && (
<Button size='small' variant='text' color='error' onClick={(event) => openConfirmAction(event, 'hardDelete')}>
Удалить
</Button>
)}
</>
)}
{!isForm && !exportMode && !taskForm.to_delete && <ExportButton onClick={handleExportTask} />}
{exportMode && <div>{isChecked ? <CheckBoxCheckSvg size='1.625rem' /> : <CheckBoxUncheckSvg size='1.625rem' />}</div>}
</Stack>
</Stack>
</TaskCard>
<Modal
open={Boolean(confirmAction)}
onClose={closeConfirmAction}
title={actionTitle}
size='small'
actions={
<>
<Button variant='grey' onClick={closeConfirmAction} disabled={actionLoading}>
Отменить
</Button>
<Button variant='contained' color={confirmAction === 'hardDelete' || confirmAction === 'mark' ? 'error' : 'primary'} onClick={runConfirmedAction} disabled={actionLoading}>
Подтвердить
</Button>
</>
}>
{actionText}
</Modal>
</>
);
};