287 lines
8.5 KiB
JavaScript
287 lines
8.5 KiB
JavaScript
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>
|
||
</>
|
||
);
|
||
};
|