420 lines
13 KiB
TypeScript
420 lines
13 KiB
TypeScript
import { type FC, useEffect, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from '../../../../components/Toast';
|
|
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
|
|
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
|
|
import { parseApiDate, toApiDate } from "./date/utils";
|
|
import type DateObject from "react-date-object";
|
|
import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover";
|
|
import TaskDetailLabelsPopover from "./labels/TaskDetailLabelsPopover";
|
|
import type { LabelsView, TaskLabel } from "./labels/types";
|
|
import TaskDetailActionBar from "./TaskDetailActionBar";
|
|
import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
|
|
import type { TaskDetailTab } from "./types";
|
|
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
|
import TaskDetailTimePopover from "./time/TaskDetailTimePopover";
|
|
import type { TaskUser } from "./users/types";
|
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
|
import {
|
|
useCreateCheckListItem,
|
|
useCreateTaskAttachment,
|
|
useCreateTaskRemark,
|
|
useDeleteTaskRemark,
|
|
useUpdateTask,
|
|
} from "../../task/hooks/useTaskData";
|
|
import { useGetProjectUsers } from "../../project/hooks/useProjectData";
|
|
import type { ProjectUserType } from "../../project/types/ProjectTypes";
|
|
import { useSingleUpload } from "../../../service/hooks/useServiceData";
|
|
import { ErrorType } from "../../../../helpers/types";
|
|
|
|
type Props = {
|
|
activeTab: TaskDetailTab | null;
|
|
onTabChange: (tab: TaskDetailTab) => void;
|
|
taskDetail?: TaskDetailType;
|
|
taskId: string;
|
|
projectId: string;
|
|
};
|
|
|
|
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId, projectId }) => {
|
|
const { t } = useTranslation("global");
|
|
const createTaskRemark = useCreateTaskRemark();
|
|
const deleteTaskRemark = useDeleteTaskRemark();
|
|
const createCheckListItem = useCreateCheckListItem();
|
|
const createTaskAttachment = useCreateTaskAttachment();
|
|
const singleUpload = useSingleUpload();
|
|
const updateTask = useUpdateTask();
|
|
const isUsersOpen = activeTab === "users";
|
|
const [userSearch, setUserSearch] = useState("");
|
|
const { data: usersData, isPending: isUsersPending } = useGetProjectUsers(projectId, isUsersOpen);
|
|
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
|
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
|
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
|
|
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set());
|
|
const [startDate, setStartDate] = useState<DateObject | null>(null);
|
|
const [endDate, setEndDate] = useState<DateObject | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!taskDetail) return;
|
|
|
|
const taskLabels = taskDetail.labels ?? taskDetail.remarks ?? [];
|
|
|
|
if (taskLabels.length) {
|
|
setLabels(taskLabels);
|
|
setSelectedLabelIds(new Set(taskLabels.map((label) => label.id)));
|
|
} else {
|
|
setLabels([]);
|
|
setSelectedLabelIds(new Set());
|
|
}
|
|
|
|
if (taskDetail.users?.length) {
|
|
setSelectedUserIds(new Set(taskDetail.users.map((user) => user.id)));
|
|
} else {
|
|
setSelectedUserIds(new Set());
|
|
}
|
|
|
|
setStartDate(parseApiDate(taskDetail.startDate));
|
|
setEndDate(parseApiDate(taskDetail.endDate));
|
|
}, [taskDetail]);
|
|
|
|
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
|
|
const projectUsers = useMemo((): TaskUser[] => {
|
|
const apiUsers: TaskUser[] =
|
|
usersData?.data?.map((user: ProjectUserType) => ({
|
|
id: user.id,
|
|
name: `${user.firstName} ${user.lastName}`.trim(),
|
|
avatar: user.profilePic,
|
|
})) ?? [];
|
|
const taskUsers: TaskUser[] =
|
|
taskDetail?.users?.map((user) => ({
|
|
id: user.id,
|
|
name: `${user.firstName} ${user.lastName}`.trim(),
|
|
avatar: user.profilePic,
|
|
})) ?? [];
|
|
const userMap = new Map<string, TaskUser>(apiUsers.map((user) => [user.id, user]));
|
|
|
|
taskUsers.forEach((user) => {
|
|
const existing = userMap.get(user.id);
|
|
|
|
if (existing) {
|
|
if (!existing.avatar && user.avatar) {
|
|
userMap.set(user.id, { ...existing, avatar: user.avatar });
|
|
}
|
|
return;
|
|
}
|
|
|
|
userMap.set(user.id, user);
|
|
});
|
|
|
|
return Array.from(userMap.values());
|
|
}, [usersData?.data, taskDetail?.users]);
|
|
const listUsers = useMemo((): TaskUser[] => {
|
|
const query = userSearch.trim().toLowerCase();
|
|
const apiUsers: TaskUser[] =
|
|
usersData?.data?.map((user: ProjectUserType) => ({
|
|
id: user.id,
|
|
name: `${user.firstName} ${user.lastName}`.trim(),
|
|
avatar: user.profilePic,
|
|
})) ?? [];
|
|
|
|
if (!query) return apiUsers;
|
|
|
|
return apiUsers.filter((user) => user.name.toLowerCase().includes(query));
|
|
}, [usersData?.data, userSearch]);
|
|
const selectedUsers = projectUsers.filter((user) => selectedUserIds.has(user.id));
|
|
const isLabelsOpen = activeTab === "labels";
|
|
const isChecklistOpen = activeTab === "checklist";
|
|
const isAttachmentOpen = activeTab === "attachment";
|
|
const isDateOpen = activeTab === "date";
|
|
const isTimeOpen = activeTab === "time";
|
|
|
|
const handleTabChange = (tab: TaskDetailTab) => {
|
|
if (tab === "labels" && activeTab !== "labels") {
|
|
setLabelsView("list");
|
|
}
|
|
onTabChange(tab);
|
|
};
|
|
|
|
const handleAddLabelClick = () => {
|
|
setLabelsView("create");
|
|
if (activeTab !== "labels") {
|
|
onTabChange("labels");
|
|
}
|
|
};
|
|
|
|
const handleAddUserClick = () => {
|
|
if (activeTab !== "users") {
|
|
onTabChange("users");
|
|
}
|
|
};
|
|
|
|
const handleLabelToggle = (id: string) => {
|
|
setSelectedLabelIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleUserToggle = (id: string) => {
|
|
const effectiveTaskId = taskId || taskDetail?.id;
|
|
if (!effectiveTaskId) return;
|
|
|
|
const previous = selectedUserIds;
|
|
const next = new Set(previous);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
|
|
setSelectedUserIds(next);
|
|
|
|
updateTask.mutate(
|
|
{ id: effectiveTaskId, params: { userIds: Array.from(next) } },
|
|
{
|
|
onSuccess: () => {
|
|
toast(t("success"), 'success');
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast(error.response?.data?.error.message[0], 'error');
|
|
setSelectedUserIds(previous);
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const handleCreate = (title: string, color: string) => {
|
|
const effectiveTaskId = taskId || taskDetail?.id;
|
|
if (!effectiveTaskId) return;
|
|
|
|
createTaskRemark.mutate(
|
|
{ title, color, taskId: effectiveTaskId },
|
|
{
|
|
onSuccess: (response) => {
|
|
const remark = response.data?.remark;
|
|
if (remark?.id) {
|
|
const newLabel: TaskLabel = {
|
|
id: remark.id,
|
|
title: remark.title,
|
|
color: remark.color,
|
|
};
|
|
setLabels((prev) => [...prev, newLabel]);
|
|
setSelectedLabelIds((prev) => new Set([...prev, newLabel.id]));
|
|
}
|
|
setLabelsView("list");
|
|
toast(t("success"), 'success');
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast(error.response?.data?.error.message[0], 'error');
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const handleDeleteLabel = (id: string) => {
|
|
const effectiveTaskId = taskId || taskDetail?.id;
|
|
if (!effectiveTaskId) return;
|
|
|
|
deleteTaskRemark.mutate(
|
|
{ id, taskId: effectiveTaskId },
|
|
{
|
|
onSuccess: () => {
|
|
setLabels((prev) => prev.filter((label) => label.id !== id));
|
|
setSelectedLabelIds((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(id);
|
|
return next;
|
|
});
|
|
toast(t("success"), "success");
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast(error.response?.data?.error.message[0], "error");
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const handleChecklistClose = () => {
|
|
if (isChecklistOpen) onTabChange("checklist");
|
|
};
|
|
|
|
const handleChecklistCreate = (title: string) => {
|
|
const effectiveTaskId = taskId || taskDetail?.id;
|
|
if (!effectiveTaskId) return;
|
|
|
|
createCheckListItem.mutate(
|
|
{ title, isDone: false, taskId: effectiveTaskId },
|
|
{
|
|
onSuccess: () => {
|
|
handleChecklistClose();
|
|
toast(t("success"), 'success');
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast(error.response?.data?.error.message[0], 'error');
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const handleAttachmentClose = () => {
|
|
if (isAttachmentOpen) onTabChange("attachment");
|
|
};
|
|
|
|
const handleAttachmentCreate = async (data: { file?: File; title: string; linkId: string }) => {
|
|
const effectiveTaskId = taskId || taskDetail?.id;
|
|
if (!effectiveTaskId) return;
|
|
|
|
let fileUrl = data.linkId.trim();
|
|
|
|
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 || "";
|
|
const type = data.file ? "file" : "link";
|
|
|
|
createTaskAttachment.mutate(
|
|
{ file: fileUrl, title, type, taskId: effectiveTaskId },
|
|
{
|
|
onSuccess: () => {
|
|
handleAttachmentClose();
|
|
toast(t("success"), 'success');
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast(error.response?.data?.error.message[0], 'error');
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const isAttachmentSubmitting = singleUpload.isPending || createTaskAttachment.isPending;
|
|
|
|
const handleDateClose = () => {
|
|
if (isDateOpen) onTabChange("date");
|
|
};
|
|
|
|
const handleDateSubmit = (data: { startDate: DateObject | null; endDate: DateObject | null }) => {
|
|
const effectiveTaskId = taskId || taskDetail?.id;
|
|
if (!effectiveTaskId) return;
|
|
|
|
const previousStartDate = startDate;
|
|
const previousEndDate = endDate;
|
|
const params = {
|
|
startDate: data.startDate ? toApiDate(data.startDate) : null,
|
|
endDate: data.endDate ? toApiDate(data.endDate) : null,
|
|
};
|
|
|
|
setStartDate(data.startDate);
|
|
setEndDate(data.endDate);
|
|
|
|
updateTask.mutate(
|
|
{ id: effectiveTaskId, params },
|
|
{
|
|
onSuccess: () => {
|
|
handleDateClose();
|
|
toast(t("success"), 'success');
|
|
},
|
|
onError: (error: ErrorType) => {
|
|
toast(error.response?.data?.error.message[0], 'error');
|
|
setStartDate(previousStartDate);
|
|
setEndDate(previousEndDate);
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
<TaskDetailActionBar
|
|
activeTab={activeTab}
|
|
onTabChange={handleTabChange}
|
|
labelsPopover={
|
|
isLabelsOpen ? (
|
|
<TaskDetailLabelsPopover
|
|
view={labelsView}
|
|
labels={labels}
|
|
selectedIds={selectedLabelIds}
|
|
onViewChange={setLabelsView}
|
|
onToggle={handleLabelToggle}
|
|
onCreate={handleCreate}
|
|
onDelete={handleDeleteLabel}
|
|
isSubmitting={createTaskRemark.isPending}
|
|
isDeleting={deleteTaskRemark.isPending}
|
|
/>
|
|
) : null
|
|
}
|
|
usersPopover={
|
|
isUsersOpen ? (
|
|
<TaskDetailUsersPopover
|
|
users={listUsers}
|
|
selectedIds={selectedUserIds}
|
|
onToggle={handleUserToggle}
|
|
isLoading={isUsersPending}
|
|
isSubmitting={updateTask.isPending}
|
|
search={userSearch}
|
|
onSearchChange={setUserSearch}
|
|
/>
|
|
) : null
|
|
}
|
|
checklistPopover={
|
|
isChecklistOpen ? (
|
|
<TaskDetailChecklistPopover
|
|
onClose={handleChecklistClose}
|
|
onSubmit={handleChecklistCreate}
|
|
isSubmitting={createCheckListItem.isPending}
|
|
/>
|
|
) : null
|
|
}
|
|
attachmentPopover={
|
|
isAttachmentOpen ? (
|
|
<TaskDetailAttachmentPopover
|
|
onClose={handleAttachmentClose}
|
|
onSubmit={handleAttachmentCreate}
|
|
isSubmitting={isAttachmentSubmitting}
|
|
/>
|
|
) : null
|
|
}
|
|
datePopover={
|
|
isDateOpen ? (
|
|
<TaskDetailDatePopover
|
|
onClose={handleDateClose}
|
|
onSubmit={handleDateSubmit}
|
|
initialStartDate={startDate}
|
|
initialEndDate={endDate}
|
|
isSubmitting={updateTask.isPending}
|
|
/>
|
|
) : null
|
|
}
|
|
timePopover={isTimeOpen ? <TaskDetailTimePopover taskId={taskId} /> : null}
|
|
/>
|
|
|
|
<TaskDetailMetadataPreview
|
|
selectedLabels={selectedLabels}
|
|
selectedUsers={selectedUsers}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
ticketId={taskDetail?.ticketId}
|
|
onAddLabel={handleAddLabelClick}
|
|
onAddUser={handleAddUserClick}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TaskDetailToolbar;
|