Files
danak-admin/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx
T
hamid zarghami 77475f09cd
deploy to danak / build_and_deploy (push) Has been cancelled
project users
2026-07-25 15:28:09 +03:30

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;