manager users

This commit is contained in:
hamid zarghami
2026-07-12 11:43:56 +03:30
parent ee8da0b88c
commit 61282e1d1b
4 changed files with 103 additions and 32 deletions
@@ -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<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
const { t } = useTranslation("global");
const createTaskRemark = useCreateTaskRemark();
const updateTask = useUpdateTask();
const getUsers = useGetUsers("");
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(["1"]));
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set());
const [checklists, setChecklists] = useState<TaskChecklist[]>(DEFAULT_CHECKLISTS);
const [startDate, setStartDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.startDate);
const [endDate, setEndDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.endDate);
@@ -61,6 +63,8 @@ const TaskDetailToolbar: FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ activeTab, onTabChange, taskDetail, task
};
const handleUserToggle = (id: string) => {
setSelectedUserIds((prev) => {
const next = new Set(prev);
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);
}
return next;
});
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<Props> = ({ activeTab, onTabChange, taskDetail, task
}
usersPopover={
isUsersOpen ? (
<TaskDetailUsersPopover users={users} selectedIds={selectedUserIds} onToggle={handleUserToggle} />
<TaskDetailUsersPopover
users={users}
selectedIds={selectedUserIds}
onToggle={handleUserToggle}
isLoading={getUsers.isPending}
isSubmitting={updateTask.isPending}
/>
) : null
}
checklistPopover={
@@ -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<string>;
onToggle: (id: string) => void;
isLoading?: boolean;
isSubmitting?: boolean;
};
const TaskDetailUsersList: FC<Props> = ({ users }) => {
const TaskDetailUsersList: FC<Props> = ({ users, selectedIds, onToggle, isLoading = false, isSubmitting = false }) => {
const { t } = useTranslation("global");
const [search, setSearch] = useState("");
@@ -30,17 +33,31 @@ const TaskDetailUsersList: FC<Props> = ({ users }) => {
<div>
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.members")}</div>
<div className="flex flex-col gap-2">
{isLoading ? (
<div className="text-[10px] text-description text-center py-2">{t("loading")}</div>
) : (
<div className="flex flex-col gap-2 max-h-48 overflow-y-auto">
{filteredUsers.map((user) => {
const isSelected = selectedIds.has(user.id);
return (
<div key={user.id} className="flex gap-1.5 items-center">
<div className="size-6 rounded-full bg-gray-400"></div>
<TaskDetailLabelCheckbox
checked={isSelected}
onToggle={() => onToggle(user.id)}
ariaLabel={user.name}
/>
<div className="size-6 rounded-full bg-gray-400 shrink-0"></div>
<div className="text-[10px]">{user.name}</div>
</div>
);
})}
{!filteredUsers.length ? <div className="text-[10px] text-description">{t("taskmanager.no_users_added")}</div> : null}
</div>
)}
</div>
{isSubmitting ? <div className="text-[10px] text-description text-center">{t("loading")}</div> : null}
</div>
);
};
@@ -6,10 +6,20 @@ type Props = {
users: TaskUser[];
selectedIds: Set<string>;
onToggle: (id: string) => void;
isLoading?: boolean;
isSubmitting?: boolean;
};
const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle }) => {
return <TaskDetailUsersList users={users} selectedIds={selectedIds} onToggle={onToggle} />;
const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle, isLoading, isSubmitting }) => {
return (
<TaskDetailUsersList
users={users}
selectedIds={selectedIds}
onToggle={onToggle}
isLoading={isLoading}
isSubmitting={isSubmitting}
/>
);
};
export default TaskDetailUsersPopover;
@@ -79,7 +79,10 @@ export type TaskDetailType = {
export type UpdateTaskType = Partial<
Pick<TaskDetailType, "title" | "description" | "taskPhaseId" | "order" | "startDate" | "endDate">
>;
> & {
color?: string;
userIds?: string[];
};
export interface GetTaskDetailResponse extends IResponse<TaskDetailType> {
statusCode: number;