manager users
This commit is contained in:
@@ -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 { useTranslation } from "react-i18next";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import { useGetUsers } from "../../../users/hooks/useUserData";
|
||||||
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
|
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
|
||||||
import { DEFAULT_DATE_RANGE } from "./date/constants";
|
import { DEFAULT_DATE_RANGE } from "./date/constants";
|
||||||
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
|
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
|
||||||
@@ -16,10 +17,9 @@ import type { LabelsView, TaskLabel } from "./labels/types";
|
|||||||
import TaskDetailActionBar from "./TaskDetailActionBar";
|
import TaskDetailActionBar from "./TaskDetailActionBar";
|
||||||
import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
|
import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
|
||||||
import type { TaskDetailTab } from "./types";
|
import type { TaskDetailTab } from "./types";
|
||||||
import { DEFAULT_USERS } from "./users/constants";
|
|
||||||
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
||||||
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
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";
|
import { ErrorType } from "../../../../helpers/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -38,10 +38,12 @@ const parseDate = (value?: string): DateObject | null => {
|
|||||||
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
|
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const createTaskRemark = useCreateTaskRemark();
|
const createTaskRemark = useCreateTaskRemark();
|
||||||
|
const updateTask = useUpdateTask();
|
||||||
|
const getUsers = useGetUsers("");
|
||||||
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
||||||
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
||||||
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
|
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 [checklists, setChecklists] = useState<TaskChecklist[]>(DEFAULT_CHECKLISTS);
|
||||||
const [startDate, setStartDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.startDate);
|
const [startDate, setStartDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.startDate);
|
||||||
const [endDate, setEndDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.endDate);
|
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) {
|
if (taskDetail.users?.length) {
|
||||||
setSelectedUserIds(new Set(taskDetail.users.map((user) => user.id)));
|
setSelectedUserIds(new Set(taskDetail.users.map((user) => user.id)));
|
||||||
|
} else {
|
||||||
|
setSelectedUserIds(new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taskDetail.checklists?.length) {
|
if (taskDetail.checklists?.length) {
|
||||||
@@ -74,12 +78,27 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
}, [taskDetail]);
|
}, [taskDetail]);
|
||||||
|
|
||||||
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
|
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
|
||||||
const users = taskDetail?.users?.length
|
const users = useMemo(() => {
|
||||||
? taskDetail.users.map((user) => ({
|
const apiUsers =
|
||||||
|
getUsers.data?.data?.users?.map((user) => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: `${user.firstName} ${user.lastName}`.trim(),
|
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 selectedUsers = users.filter((user) => selectedUserIds.has(user.id));
|
||||||
const isLabelsOpen = activeTab === "labels";
|
const isLabelsOpen = activeTab === "labels";
|
||||||
const isUsersOpen = activeTab === "users";
|
const isUsersOpen = activeTab === "users";
|
||||||
@@ -107,15 +126,31 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUserToggle = (id: string) => {
|
const handleUserToggle = (id: string) => {
|
||||||
setSelectedUserIds((prev) => {
|
const effectiveTaskId = taskId || taskDetail?.id;
|
||||||
const next = new Set(prev);
|
if (!effectiveTaskId) return;
|
||||||
if (next.has(id)) {
|
|
||||||
next.delete(id);
|
const previous = selectedUserIds;
|
||||||
} else {
|
const next = new Set(previous);
|
||||||
next.add(id);
|
if (next.has(id)) {
|
||||||
}
|
next.delete(id);
|
||||||
return next;
|
} 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) => {
|
const handleCreate = (title: string, color: string) => {
|
||||||
@@ -200,7 +235,13 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
}
|
}
|
||||||
usersPopover={
|
usersPopover={
|
||||||
isUsersOpen ? (
|
isUsersOpen ? (
|
||||||
<TaskDetailUsersPopover users={users} selectedIds={selectedUserIds} onToggle={handleUserToggle} />
|
<TaskDetailUsersPopover
|
||||||
|
users={users}
|
||||||
|
selectedIds={selectedUserIds}
|
||||||
|
onToggle={handleUserToggle}
|
||||||
|
isLoading={getUsers.isPending}
|
||||||
|
isSubmitting={updateTask.isPending}
|
||||||
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
checklistPopover={
|
checklistPopover={
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { type FC, useMemo, useState } from "react";
|
import { type FC, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Input from "../../../../../components/Input";
|
import Input from "../../../../../components/Input";
|
||||||
|
import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox";
|
||||||
import type { TaskUser } from "./types";
|
import type { TaskUser } from "./types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
users: TaskUser[];
|
users: TaskUser[];
|
||||||
selectedIds: Set<string>;
|
selectedIds: Set<string>;
|
||||||
onToggle: (id: string) => void;
|
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 { t } = useTranslation("global");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
@@ -30,17 +33,31 @@ const TaskDetailUsersList: FC<Props> = ({ users }) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.members")}</div>
|
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.members")}</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
{isLoading ? (
|
||||||
{filteredUsers.map((user) => {
|
<div className="text-[10px] text-description text-center py-2">{t("loading")}</div>
|
||||||
return (
|
) : (
|
||||||
<div key={user.id} className="flex gap-1.5 items-center">
|
<div className="flex flex-col gap-2 max-h-48 overflow-y-auto">
|
||||||
<div className="size-6 rounded-full bg-gray-400"></div>
|
{filteredUsers.map((user) => {
|
||||||
<div className="text-[10px]">{user.name}</div>
|
const isSelected = selectedIds.has(user.id);
|
||||||
</div>
|
|
||||||
);
|
return (
|
||||||
})}
|
<div key={user.id} className="flex gap-1.5 items-center">
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
|
{isSubmitting ? <div className="text-[10px] text-description text-center">{t("loading")}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,10 +6,20 @@ type Props = {
|
|||||||
users: TaskUser[];
|
users: TaskUser[];
|
||||||
selectedIds: Set<string>;
|
selectedIds: Set<string>;
|
||||||
onToggle: (id: string) => void;
|
onToggle: (id: string) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
isSubmitting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle }) => {
|
const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle, isLoading, isSubmitting }) => {
|
||||||
return <TaskDetailUsersList users={users} selectedIds={selectedIds} onToggle={onToggle} />;
|
return (
|
||||||
|
<TaskDetailUsersList
|
||||||
|
users={users}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onToggle={onToggle}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TaskDetailUsersPopover;
|
export default TaskDetailUsersPopover;
|
||||||
|
|||||||
@@ -79,7 +79,10 @@ export type TaskDetailType = {
|
|||||||
|
|
||||||
export type UpdateTaskType = Partial<
|
export type UpdateTaskType = Partial<
|
||||||
Pick<TaskDetailType, "title" | "description" | "taskPhaseId" | "order" | "startDate" | "endDate">
|
Pick<TaskDetailType, "title" | "description" | "taskPhaseId" | "order" | "startDate" | "endDate">
|
||||||
>;
|
> & {
|
||||||
|
color?: string;
|
||||||
|
userIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface GetTaskDetailResponse extends IResponse<TaskDetailType> {
|
export interface GetTaskDetailResponse extends IResponse<TaskDetailType> {
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user