diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailModal.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailModal.tsx index 7666da0..1c8a015 100644 --- a/src/pages/taskmanager/components/task-detail/TaskDetailModal.tsx +++ b/src/pages/taskmanager/components/task-detail/TaskDetailModal.tsx @@ -209,7 +209,7 @@ const TaskDetailModal: FC = ({ open, task, columns, projectId, onClose, o isSaving={updateTask.isPending} /> - + = ({ title, createdAt, type, url, onEdit, onDele - + {({ close }) => ( + <> + - + + + )} diff --git a/src/pages/taskmanager/components/task-detail/attachment/List.tsx b/src/pages/taskmanager/components/task-detail/attachment/List.tsx index 3d69ccb..90cae34 100644 --- a/src/pages/taskmanager/components/task-detail/attachment/List.tsx +++ b/src/pages/taskmanager/components/task-detail/attachment/List.tsx @@ -1,15 +1,28 @@ -import { type FC } from "react"; +import { useState, type FC } from "react"; import { useTranslation } from "react-i18next"; -import AttachmentItem from "./AttachmentItem"; -import type { TaskAttachment } from "./types"; +import DefaulModal from "../../../../../components/DefaulModal"; +import ModalConfrim from "../../../../../components/ModalConfrim"; +import { toast } from "../../../../../components/Toast"; +import { ErrorType } from "../../../../../helpers/types"; +import { useSingleUpload } from "../../../../service/hooks/useServiceData"; +import { useDeleteTaskAttachment, useUpdateTaskAttachment } from "../../../task/hooks/useTaskData"; import type { TaskAttachmentType } from "../../../task/types/TaskTypes"; +import AttachmentItem from "./AttachmentItem"; +import TaskDetailAttachmentForm from "./TaskDetailAttachmentForm"; +import type { TaskAttachment } from "./types"; type Props = { + taskId: string; attachments?: TaskAttachmentType[]; }; -const AttachmentList: FC = ({ attachments = [] }) => { +const AttachmentList: FC = ({ taskId, attachments = [] }) => { const { t } = useTranslation("global"); + const singleUpload = useSingleUpload(); + const updateTaskAttachment = useUpdateTaskAttachment(); + const deleteTaskAttachment = useDeleteTaskAttachment(); + const [deleteItemId, setDeleteItemId] = useState(null); + const [editItem, setEditItem] = useState(null); const mappedAttachments: TaskAttachment[] = attachments.map((item) => ({ id: item.id, @@ -22,6 +35,61 @@ const AttachmentList: FC = ({ attachments = [] }) => { const files = mappedAttachments.filter((item) => item.type === "file"); const links = mappedAttachments.filter((item) => item.type === "link"); + const handleDelete = () => { + if (!deleteItemId) return; + + deleteTaskAttachment.mutate( + { id: deleteItemId, taskId }, + { + onSuccess: () => { + setDeleteItemId(null); + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error.message[0], "error"); + }, + }, + ); + }; + + const handleUpdate = async (data: { file?: File; title: string; linkId: string }) => { + if (!editItem) return; + + let fileUrl = data.linkId.trim() || editItem.file || ""; + + if (data.file) { + const formData = new FormData(); + formData.append("file", data.file); + + try { + const uploadResult = await singleUpload.mutateAsync(formData); + fileUrl = uploadResult?.data?.url ?? ""; + } catch (error) { + toast((error as ErrorType).response?.data?.error.message[0], "error"); + return; + } + } + + if (!fileUrl) return; + + const title = data.title.trim() || data.file?.name || editItem.title; + const type = data.file ? "file" : editItem.type; + + updateTaskAttachment.mutate( + { id: editItem.id, params: { file: fileUrl, title, type, taskId } }, + { + onSuccess: () => { + setEditItem(null); + toast(t("success"), "success"); + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error.message[0], "error"); + }, + }, + ); + }; + + const isUpdateSubmitting = singleUpload.isPending || updateTaskAttachment.isPending; + if (mappedAttachments.length === 0) return null; return ( @@ -40,8 +108,8 @@ const AttachmentList: FC = ({ attachments = [] }) => { createdAt={item.createdAt} type="file" url={item.file} - onEdit={() => {}} - onDelete={() => {}} + onEdit={() => setEditItem(item)} + onDelete={() => setDeleteItemId(item.id)} /> ))} @@ -60,13 +128,38 @@ const AttachmentList: FC = ({ attachments = [] }) => { createdAt={item.createdAt} type="link" url={item.file} - onEdit={() => {}} - onDelete={() => {}} + onEdit={() => setEditItem(item)} + onDelete={() => setDeleteItemId(item.id)} /> ))} )} + + setDeleteItemId(null)} + onConfrim={handleDelete} + isLoading={deleteTaskAttachment.isPending} + /> + + setEditItem(null)} + isHeader + title_header={t("edit")} + width={420} + > + {editItem ? ( + setEditItem(null)} + onSubmit={handleUpdate} + isSubmitting={isUpdateSubmitting} + submitLabel={t("save")} + /> + ) : null} + ); }; diff --git a/src/pages/taskmanager/components/task-detail/attachment/TaskDetailAttachmentForm.tsx b/src/pages/taskmanager/components/task-detail/attachment/TaskDetailAttachmentForm.tsx index cba3e4b..1a28403 100644 --- a/src/pages/taskmanager/components/task-detail/attachment/TaskDetailAttachmentForm.tsx +++ b/src/pages/taskmanager/components/task-detail/attachment/TaskDetailAttachmentForm.tsx @@ -4,22 +4,39 @@ import Button from "../../../../../components/Button"; import Input from "../../../../../components/Input"; import Select from "../../../../../components/Select"; +type InitialValues = { + title: string; + fileUrl?: string; + type: "file" | "link"; +}; + type Props = { onClose: () => void; onSubmit: (data: { file?: File; title: string; linkId: string }) => void; isSubmitting?: boolean; + initialValues?: InitialValues; + submitLabel?: string; }; -const TaskDetailAttachmentForm: FC = ({ onClose, onSubmit, isSubmitting = false }) => { +const TaskDetailAttachmentForm: FC = ({ + onClose, + onSubmit, + isSubmitting = false, + initialValues, + submitLabel, +}) => { const { t } = useTranslation("global"); const fileInputRef = useRef(null); const [file, setFile] = useState(null); - const [title, setTitle] = useState(""); - const [linkId, setLinkId] = useState(""); + const [title, setTitle] = useState(initialValues?.title ?? ""); + const [linkId, setLinkId] = useState(initialValues?.type === "link" ? (initialValues.fileUrl ?? "") : ""); + const isEdit = Boolean(initialValues); const linkItems = [{ value: "", label: t("taskmanager.task_detail.none") }]; - const canSubmit = Boolean(file) || Boolean(title.trim()); + const canSubmit = isEdit + ? Boolean(title.trim()) || Boolean(file) + : Boolean(file) || Boolean(title.trim()); const handleFileChange = (e: React.ChangeEvent) => { const selected = e.target.files?.[0]; @@ -35,6 +52,12 @@ const TaskDetailAttachmentForm: FC = ({ onClose, onSubmit, isSubmitting = }); }; + const currentFileLabel = file + ? file.name + : initialValues?.fileUrl + ? initialValues.fileUrl + : t("no_select_file"); + return (

{t("taskmanager.task_detail.attachment")}

@@ -43,7 +66,7 @@ const TaskDetailAttachmentForm: FC = ({ onClose, onSubmit, isSubmitting = - {file ? file.name : t("no_select_file")} + {currentFileLabel}
@@ -69,7 +92,7 @@ const TaskDetailAttachmentForm: FC = ({ onClose, onSubmit, isSubmitting = {t("cancel")} diff --git a/src/pages/taskmanager/task/hooks/useTaskData.ts b/src/pages/taskmanager/task/hooks/useTaskData.ts index ec1ef75..636f098 100644 --- a/src/pages/taskmanager/task/hooks/useTaskData.ts +++ b/src/pages/taskmanager/task/hooks/useTaskData.ts @@ -7,6 +7,7 @@ import { CreateTaskRemarkType, CreateTaskType, UpdateCheckListItemType, + UpdateTaskAttachmentType, UpdateTaskType, } from "../types/TaskTypes"; @@ -169,3 +170,30 @@ export const useCreateTaskAttachment = () => { }, }); }; + +export const useUpdateTaskAttachment = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, params }: { id: string; params: UpdateTaskAttachmentType }) => + api.updateTaskAttachment(id, params), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ["task-detail", variables.params.taskId] }); + queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] }); + queryClient.invalidateQueries({ queryKey: ["project"] }); + }, + }); +}; + +export const useDeleteTaskAttachment = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id }: { id: string; taskId: string }) => api.deleteTaskAttachment(id), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] }); + queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] }); + queryClient.invalidateQueries({ queryKey: ["project"] }); + }, + }); +}; diff --git a/src/pages/taskmanager/task/service/TaskService.ts b/src/pages/taskmanager/task/service/TaskService.ts index 76a886e..b96f987 100644 --- a/src/pages/taskmanager/task/service/TaskService.ts +++ b/src/pages/taskmanager/task/service/TaskService.ts @@ -11,6 +11,7 @@ import { GetTasksByTaskPhaseResponse, UpdateCheckListItemResponse, UpdateCheckListItemType, + UpdateTaskAttachmentType, UpdateTaskType, } from "../types/TaskTypes"; @@ -85,3 +86,13 @@ export const createTaskAttachment = async (params: CreateTaskAttachmentType) => const { data } = await axios.post(`/task-manager/attachments`, params); return data; }; + +export const updateTaskAttachment = async (id: string, params: UpdateTaskAttachmentType) => { + const { data } = await axios.patch(`/task-manager/attachments/${id}`, params); + return data; +}; + +export const deleteTaskAttachment = async (id: string) => { + const { data } = await axios.delete(`/task-manager/attachments/${id}`); + return data; +}; diff --git a/src/pages/taskmanager/task/types/TaskTypes.ts b/src/pages/taskmanager/task/types/TaskTypes.ts index cead948..5614a20 100644 --- a/src/pages/taskmanager/task/types/TaskTypes.ts +++ b/src/pages/taskmanager/task/types/TaskTypes.ts @@ -111,6 +111,13 @@ export type CreateTaskAttachmentType = { taskId: string; }; +export type UpdateTaskAttachmentType = { + file: string; + title: string; + type: "file" | "link"; + taskId: string; +}; + export type TaskDetailType = { id: string; createdAt: string;