From 61282e1d1b88d23812b30362ff817875711f147f Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 12 Jul 2026 11:43:56 +0330 Subject: [PATCH] manager users --- .../task-detail/TaskDetailToolbar.tsx | 77 ++++++++++++++----- .../task-detail/users/TaskDetailUsersList.tsx | 39 +++++++--- .../users/TaskDetailUsersPopover.tsx | 14 +++- src/pages/taskmanager/task/types/TaskTypes.ts | 5 +- 4 files changed, 103 insertions(+), 32 deletions(-) diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx index 7eb248d..adf2af8 100644 --- a/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx +++ b/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx @@ -1,6 +1,7 @@ -import { type FC, useEffect, useState } from "react"; +import { type FC, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; +import { useGetUsers } from "../../../users/hooks/useUserData"; import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover"; import { DEFAULT_DATE_RANGE } from "./date/constants"; import TaskDetailDatePopover from "./date/TaskDetailDatePopover"; @@ -16,10 +17,9 @@ import type { LabelsView, TaskLabel } from "./labels/types"; import TaskDetailActionBar from "./TaskDetailActionBar"; import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview"; import type { TaskDetailTab } from "./types"; -import { DEFAULT_USERS } from "./users/constants"; import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover"; import type { TaskDetailType } from "../../task/types/TaskTypes"; -import { useCreateTaskRemark } from "../../task/hooks/useTaskData"; +import { useCreateTaskRemark, useUpdateTask } from "../../task/hooks/useTaskData"; import { ErrorType } from "../../../../helpers/types"; type Props = { @@ -38,10 +38,12 @@ const parseDate = (value?: string): DateObject | null => { const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, taskId }) => { const { t } = useTranslation("global"); const createTaskRemark = useCreateTaskRemark(); + const updateTask = useUpdateTask(); + const getUsers = useGetUsers(""); const [labelsView, setLabelsView] = useState("list"); const [labels, setLabels] = useState([]); const [selectedLabelIds, setSelectedLabelIds] = useState>(new Set()); - const [selectedUserIds, setSelectedUserIds] = useState>(new Set(["1"])); + const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); const [checklists, setChecklists] = useState(DEFAULT_CHECKLISTS); const [startDate, setStartDate] = useState(DEFAULT_DATE_RANGE.startDate); const [endDate, setEndDate] = useState(DEFAULT_DATE_RANGE.endDate); @@ -61,6 +63,8 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task if (taskDetail.users?.length) { setSelectedUserIds(new Set(taskDetail.users.map((user) => user.id))); + } else { + setSelectedUserIds(new Set()); } if (taskDetail.checklists?.length) { @@ -74,12 +78,27 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task }, [taskDetail]); const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id)); - const users = taskDetail?.users?.length - ? taskDetail.users.map((user) => ({ + const users = useMemo(() => { + const apiUsers = + getUsers.data?.data?.users?.map((user) => ({ id: user.id, name: `${user.firstName} ${user.lastName}`.trim(), - })) - : DEFAULT_USERS; + })) ?? []; + const taskUsers = + taskDetail?.users?.map((user) => ({ + id: user.id, + name: `${user.firstName} ${user.lastName}`.trim(), + })) ?? []; + const userMap = new Map(apiUsers.map((user) => [user.id, user])); + + taskUsers.forEach((user) => { + if (!userMap.has(user.id)) { + userMap.set(user.id, user); + } + }); + + return Array.from(userMap.values()); + }, [getUsers.data, taskDetail?.users]); const selectedUsers = users.filter((user) => selectedUserIds.has(user.id)); const isLabelsOpen = activeTab === "labels"; const isUsersOpen = activeTab === "users"; @@ -107,15 +126,31 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task }; const handleUserToggle = (id: string) => { - setSelectedUserIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); + 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.success(t("success")); + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error.message[0]); + setSelectedUserIds(previous); + }, + }, + ); }; const handleCreate = (title: string, color: string) => { @@ -200,7 +235,13 @@ const TaskDetailToolbar: FC = ({ activeTab, onTabChange, taskDetail, task } usersPopover={ isUsersOpen ? ( - + ) : null } checklistPopover={ diff --git a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersList.tsx b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersList.tsx index 8a4ee16..d44b143 100644 --- a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersList.tsx +++ b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersList.tsx @@ -1,15 +1,18 @@ import { type FC, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import Input from "../../../../../components/Input"; +import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox"; import type { TaskUser } from "./types"; type Props = { users: TaskUser[]; selectedIds: Set; onToggle: (id: string) => void; + isLoading?: boolean; + isSubmitting?: boolean; }; -const TaskDetailUsersList: FC = ({ users }) => { +const TaskDetailUsersList: FC = ({ users, selectedIds, onToggle, isLoading = false, isSubmitting = false }) => { const { t } = useTranslation("global"); const [search, setSearch] = useState(""); @@ -30,17 +33,31 @@ const TaskDetailUsersList: FC = ({ users }) => {
{t("taskmanager.task_detail.members")}
-
- {filteredUsers.map((user) => { - return ( -
-
-
{user.name}
-
- ); - })} -
+ {isLoading ? ( +
{t("loading")}
+ ) : ( +
+ {filteredUsers.map((user) => { + const isSelected = selectedIds.has(user.id); + + return ( +
+ onToggle(user.id)} + ariaLabel={user.name} + /> +
+
{user.name}
+
+ ); + })} + {!filteredUsers.length ?
{t("taskmanager.no_users_added")}
: null} +
+ )}
+ + {isSubmitting ?
{t("loading")}
: null} ); }; diff --git a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx index 1ddc7f2..be820f3 100644 --- a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx +++ b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx @@ -6,10 +6,20 @@ type Props = { users: TaskUser[]; selectedIds: Set; onToggle: (id: string) => void; + isLoading?: boolean; + isSubmitting?: boolean; }; -const TaskDetailUsersPopover: FC = ({ users, selectedIds, onToggle }) => { - return ; +const TaskDetailUsersPopover: FC = ({ users, selectedIds, onToggle, isLoading, isSubmitting }) => { + return ( + + ); }; export default TaskDetailUsersPopover; diff --git a/src/pages/taskmanager/task/types/TaskTypes.ts b/src/pages/taskmanager/task/types/TaskTypes.ts index ab30a78..4e34564 100644 --- a/src/pages/taskmanager/task/types/TaskTypes.ts +++ b/src/pages/taskmanager/task/types/TaskTypes.ts @@ -79,7 +79,10 @@ export type TaskDetailType = { export type UpdateTaskType = Partial< Pick ->; +> & { + color?: string; + userIds?: string[]; +}; export interface GetTaskDetailResponse extends IResponse { statusCode: number;