Compare commits

...

4 Commits

Author SHA1 Message Date
hamid zarghami 77475f09cd project users
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-25 15:28:09 +03:30
hamid zarghami 9413cf03c9 out side click fix 2026-07-25 12:52:49 +03:30
hamid zarghami b2f9166c22 tooltip 2026-07-25 12:46:57 +03:30
hamid zarghami 7fedfdfbfd padding left for task description 2026-07-25 12:25:48 +03:30
13 changed files with 72 additions and 59 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
VITE_TOKEN_NAME = 'admin_token' VITE_TOKEN_NAME = 'admin_token'
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token' VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
VITE_BASE_URL = 'https://api.danakcorp.com' # VITE_BASE_URL = 'https://api.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.99.131:3500' VITE_BASE_URL = 'http://192.168.99.131:3500'
@@ -5,12 +5,23 @@ type Props = {
src?: string | null; src?: string | null;
alt?: string; alt?: string;
className?: string; className?: string;
showTooltip?: boolean;
}; };
const UserAvatar: FC<Props> = ({ src, alt = "", className = "size-6" }) => ( const UserAvatar: FC<Props> = ({ src, alt = "", className = "size-6", showTooltip = true }) => (
<div className={`${className} rounded-full overflow-hidden shrink-0 bg-[#D0D0D0]`}> <div className={`relative group/avatar ${className} rounded-full overflow-visible shrink-0`}>
<div className="size-full rounded-full overflow-hidden bg-[#D0D0D0]">
<img src={src || AvatarImage} alt={alt} className="size-full object-cover" /> <img src={src || AvatarImage} alt={alt} className="size-full object-cover" />
</div> </div>
{showTooltip && alt ? (
<span
role="tooltip"
className="pointer-events-none absolute bottom-full left-1/2 z-20 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md bg-[#292D32] px-2 py-1 text-[10px] text-white opacity-0 transition-opacity group-hover/avatar:opacity-100"
>
{alt}
</span>
) : null}
</div>
); );
export default UserAvatar; export default UserAvatar;
@@ -28,7 +28,7 @@ const TaskDetailDescription: FC<Props> = ({ value = "", onChange, onSave, isSavi
onChange={(e) => onChange?.(e.target.value)} onChange={(e) => onChange?.(e.target.value)}
placeholder={t("taskmanager.task_detail.description_placeholder")} placeholder={t("taskmanager.task_detail.description_placeholder")}
rows={5} rows={5}
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 text-xs outline-none resize-none placeholder:text-description" className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 pl-20 text-xs outline-none resize-none placeholder:text-description"
/> />
</div> </div>
</div> </div>
@@ -199,6 +199,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
onTabChange={handleTabChange} onTabChange={handleTabChange}
taskDetail={taskDetail} taskDetail={taskDetail}
taskId={task.id} taskId={task.id}
projectId={taskDetail?.projectId ?? projectId}
/> />
</div> </div>
@@ -61,8 +61,10 @@ const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, wid
onClose(); onClose();
}; };
document.addEventListener("pointerdown", handlePointerDown); // Capture: DefaulModal stops pointerdown propagation on bubble, so outside-click
return () => document.removeEventListener("pointerdown", handlePointerDown); // would never reach a document bubble listener while the modal is open.
document.addEventListener("pointerdown", handlePointerDown, true);
return () => document.removeEventListener("pointerdown", handlePointerDown, true);
}, [anchorRef, onClose]); }, [anchorRef, onClose]);
if (!position) return null; if (!position) return null;
@@ -1,7 +1,6 @@
import { type FC, useEffect, useMemo, useState } from "react"; import { type FC, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from '../../../../components/Toast'; import { toast } from '../../../../components/Toast';
import { useGetUsersInfinite } from "../../../users/hooks/useUserData";
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover"; import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
import TaskDetailDatePopover from "./date/TaskDetailDatePopover"; import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
import { parseApiDate, toApiDate } from "./date/utils"; import { parseApiDate, toApiDate } from "./date/utils";
@@ -15,7 +14,6 @@ import type { TaskDetailTab } from "./types";
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover"; import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
import TaskDetailTimePopover from "./time/TaskDetailTimePopover"; import TaskDetailTimePopover from "./time/TaskDetailTimePopover";
import type { TaskUser } from "./users/types"; import type { TaskUser } from "./users/types";
import type { UserItemType } from "../../../users/types/UserTypes";
import type { TaskDetailType } from "../../task/types/TaskTypes"; import type { TaskDetailType } from "../../task/types/TaskTypes";
import { import {
useCreateCheckListItem, useCreateCheckListItem,
@@ -24,6 +22,8 @@ import {
useDeleteTaskRemark, useDeleteTaskRemark,
useUpdateTask, useUpdateTask,
} from "../../task/hooks/useTaskData"; } from "../../task/hooks/useTaskData";
import { useGetProjectUsers } from "../../project/hooks/useProjectData";
import type { ProjectUserType } from "../../project/types/ProjectTypes";
import { useSingleUpload } from "../../../service/hooks/useServiceData"; import { useSingleUpload } from "../../../service/hooks/useServiceData";
import { ErrorType } from "../../../../helpers/types"; import { ErrorType } from "../../../../helpers/types";
@@ -32,9 +32,10 @@ type Props = {
onTabChange: (tab: TaskDetailTab) => void; onTabChange: (tab: TaskDetailTab) => void;
taskDetail?: TaskDetailType; taskDetail?: TaskDetailType;
taskId: string; taskId: string;
projectId: string;
}; };
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => { const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId, projectId }) => {
const { t } = useTranslation("global"); const { t } = useTranslation("global");
const createTaskRemark = useCreateTaskRemark(); const createTaskRemark = useCreateTaskRemark();
const deleteTaskRemark = useDeleteTaskRemark(); const deleteTaskRemark = useDeleteTaskRemark();
@@ -44,14 +45,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
const updateTask = useUpdateTask(); const updateTask = useUpdateTask();
const isUsersOpen = activeTab === "users"; const isUsersOpen = activeTab === "users";
const [userSearch, setUserSearch] = useState(""); const [userSearch, setUserSearch] = useState("");
const [debouncedUserSearch, setDebouncedUserSearch] = useState(""); const { data: usersData, isPending: isUsersPending } = useGetProjectUsers(projectId, isUsersOpen);
const {
data: usersData,
isPending: isUsersPending,
isFetchingNextPage: isFetchingNextUsersPage,
fetchNextPage: fetchNextUsersPage,
hasNextPage: hasNextUsersPage,
} = useGetUsersInfinite(debouncedUserSearch, undefined, isUsersOpen);
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());
@@ -59,14 +53,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
const [startDate, setStartDate] = useState<DateObject | null>(null); const [startDate, setStartDate] = useState<DateObject | null>(null);
const [endDate, setEndDate] = useState<DateObject | null>(null); const [endDate, setEndDate] = useState<DateObject | null>(null);
useEffect(() => {
const timeoutId = window.setTimeout(() => {
setDebouncedUserSearch(userSearch.trim());
}, 300);
return () => window.clearTimeout(timeoutId);
}, [userSearch]);
useEffect(() => { useEffect(() => {
if (!taskDetail) return; if (!taskDetail) return;
@@ -91,16 +77,13 @@ 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 = useMemo((): TaskUser[] => { const projectUsers = useMemo((): TaskUser[] => {
const apiUsers: TaskUser[] = const apiUsers: TaskUser[] =
usersData?.pages.flatMap( usersData?.data?.map((user: ProjectUserType) => ({
(page) =>
page.data?.users?.map((user: UserItemType) => ({
id: user.id, id: user.id,
name: `${user.firstName} ${user.lastName}`.trim(), name: `${user.firstName} ${user.lastName}`.trim(),
avatar: user.profilePic, avatar: user.profilePic,
})) ?? [], })) ?? [];
) ?? [];
const taskUsers: TaskUser[] = const taskUsers: TaskUser[] =
taskDetail?.users?.map((user) => ({ taskDetail?.users?.map((user) => ({
id: user.id, id: user.id,
@@ -123,20 +106,21 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
}); });
return Array.from(userMap.values()); return Array.from(userMap.values());
}, [usersData?.pages, taskDetail?.users]); }, [usersData?.data, taskDetail?.users]);
const listUsers = useMemo((): TaskUser[] => { const listUsers = useMemo((): TaskUser[] => {
return ( const query = userSearch.trim().toLowerCase();
usersData?.pages.flatMap( const apiUsers: TaskUser[] =
(page) => usersData?.data?.map((user: ProjectUserType) => ({
page.data?.users?.map((user: UserItemType) => ({
id: user.id, id: user.id,
name: `${user.firstName} ${user.lastName}`.trim(), name: `${user.firstName} ${user.lastName}`.trim(),
avatar: user.profilePic, avatar: user.profilePic,
})) ?? [], })) ?? [];
) ?? []
); if (!query) return apiUsers;
}, [usersData?.pages]);
const selectedUsers = users.filter((user) => selectedUserIds.has(user.id)); 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 isLabelsOpen = activeTab === "labels";
const isChecklistOpen = activeTab === "checklist"; const isChecklistOpen = activeTab === "checklist";
const isAttachmentOpen = activeTab === "attachment"; const isAttachmentOpen = activeTab === "attachment";
@@ -382,11 +366,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
onToggle={handleUserToggle} onToggle={handleUserToggle}
isLoading={isUsersPending} isLoading={isUsersPending}
isSubmitting={updateTask.isPending} isSubmitting={updateTask.isPending}
hasNextPage={hasNextUsersPage}
isFetchingNextPage={isFetchingNextUsersPage}
onLoadMore={() => {
void fetchNextUsersPage();
}}
search={userSearch} search={userSearch}
onSearchChange={setUserSearch} onSearchChange={setUserSearch}
/> />
@@ -69,7 +69,7 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
return ( return (
<div className="bg-white/25 rounded-xl p-3"> <div className="bg-white/25 rounded-xl p-3">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<UserAvatar src={comment.user?.profilePic} alt={authorName} className="size-7" /> <UserAvatar src={comment.user?.profilePic} alt={authorName} className="size-7" showTooltip={false} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@@ -72,7 +72,7 @@ const TaskDetailUsersList: FC<Props> = ({
onToggle={() => onToggle(user.id)} onToggle={() => onToggle(user.id)}
ariaLabel={user.name} ariaLabel={user.name}
/> />
<UserAvatar src={user.avatar} alt={user.name} /> <UserAvatar src={user.avatar} alt={user.name} showTooltip={false} />
<div className="text-[10px]">{user.name}</div> <div className="text-[10px]">{user.name}</div>
</div> </div>
); );
@@ -155,7 +155,7 @@ const CreateProjectSidebar: FC<Props> = ({
) : ( ) : (
selectedUsers.map((user) => ( selectedUsers.map((user) => (
<div key={user.id} className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg"> <div key={user.id} className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg">
<UserAvatar src={user.profilePic} alt={user.name} /> <UserAvatar src={user.profilePic} alt={user.name} showTooltip={false} />
<div>{user.name}</div> <div>{user.name}</div>
<CloseCircle variant="Bold" className="size-4 cursor-pointer" color="red" onClick={() => handleRemoveUser(user.id)} /> <CloseCircle variant="Bold" className="size-4 cursor-pointer" color="red" onClick={() => handleRemoveUser(user.id)} />
</div> </div>
@@ -26,6 +26,14 @@ export const useGetProject = (id: string) => {
}); });
}; };
export const useGetProjectUsers = (id: string, enabled = true) => {
return useQuery({
queryKey: ["project-users", id],
queryFn: () => api.getProjectUsers(id),
enabled: !!id && enabled,
});
};
export const useCreateProject = () => { export const useCreateProject = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -49,6 +57,9 @@ export const useUpdateProject = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["project-detail", variables.id], queryKey: ["project-detail", variables.id],
}); });
queryClient.invalidateQueries({
queryKey: ["project-users", variables.id],
});
}, },
}); });
}; };
@@ -1,5 +1,5 @@
import axios from "../../../../config/axios"; import axios from "../../../../config/axios";
import { CreateProjectType, GetProjectResponse, UpdateProjectType } from "../types/ProjectTypes"; import { CreateProjectType, GetProjectResponse, GetProjectUsersResponse, UpdateProjectType } from "../types/ProjectTypes";
export const getProjectsByWorkspace = async (workspaceId: string) => { export const getProjectsByWorkspace = async (workspaceId: string) => {
const { data } = await axios.get(`/task-manager/projects/user/workspace/${workspaceId}`); const { data } = await axios.get(`/task-manager/projects/user/workspace/${workspaceId}`);
@@ -16,6 +16,11 @@ export const getProject = async (id: string): Promise<GetProjectResponse> => {
return data; return data;
}; };
export const getProjectUsers = async (id: string): Promise<GetProjectUsersResponse> => {
const { data } = await axios.get(`/task-manager/projects/${id}/users`);
return data;
};
export const createProject = async (params: CreateProjectType) => { export const createProject = async (params: CreateProjectType) => {
const { data } = await axios.post(`/task-manager/projects`, params); const { data } = await axios.post(`/task-manager/projects`, params);
return data; return data;
@@ -53,3 +53,7 @@ export type ProjectWithPhasesType = {
export interface GetProjectResponse extends IResponse<ProjectWithPhasesType> { export interface GetProjectResponse extends IResponse<ProjectWithPhasesType> {
statusCode: number; statusCode: number;
} }
export interface GetProjectUsersResponse extends IResponse<ProjectUserType[]> {
statusCode: number;
}
@@ -113,7 +113,7 @@ const CreateWorkspaceSidebar: FC<Props> = ({
key={user.id} key={user.id}
className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg" className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg"
> >
<UserAvatar src={user.profilePic} alt={user.name} /> <UserAvatar src={user.profilePic} alt={user.name} showTooltip={false} />
<div>{user.name}</div> <div>{user.name}</div>
<CloseCircle <CloseCircle
variant="Bold" variant="Bold"