From a4eaa50fdf79d6dad86bc6d792fe2df5f56896f2 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 4 Jul 2026 10:43:16 +0330 Subject: [PATCH] group --- src/pages/customers/groups/Update.tsx | 3 + .../groups/components/AddUsersModal.tsx | 144 ++++++++++++++++++ .../components/GroupUserTableColumns.tsx | 40 +++++ .../groups/components/GroupUsersSection.tsx | 72 +++++++++ .../groups/hooks/useUserGroupData.ts | 50 +++++- .../groups/service/UserGroupService.ts | 29 ++++ src/pages/customers/groups/types/Types.ts | 7 + 7 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 src/pages/customers/groups/components/AddUsersModal.tsx create mode 100644 src/pages/customers/groups/components/GroupUserTableColumns.tsx create mode 100644 src/pages/customers/groups/components/GroupUsersSection.tsx diff --git a/src/pages/customers/groups/Update.tsx b/src/pages/customers/groups/Update.tsx index 6145136..3c92f7d 100644 --- a/src/pages/customers/groups/Update.tsx +++ b/src/pages/customers/groups/Update.tsx @@ -12,6 +12,7 @@ import { toast } from "react-toastify"; import * as Yup from "yup"; import { useGetUserGroup, useUpdateUserGroup } from "./hooks/useUserGroupData"; import type { CreateUserGroupType } from "./types/Types"; +import GroupUsersSection from "./components/GroupUsersSection"; const UpdateUserGroup: FC = () => { const navigate = useNavigate(); @@ -84,6 +85,8 @@ const UpdateUserGroup: FC = () => { + + {id && } ); }; diff --git a/src/pages/customers/groups/components/AddUsersModal.tsx b/src/pages/customers/groups/components/AddUsersModal.tsx new file mode 100644 index 0000000..d4b71e6 --- /dev/null +++ b/src/pages/customers/groups/components/AddUsersModal.tsx @@ -0,0 +1,144 @@ +import Button from "@/components/Button"; +import DefaulModal from "@/components/DefaulModal"; +import Input from "@/components/Input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { extractErrorMessage } from "@/config/func"; +import type { ErrorType } from "@/helpers/types"; +import { useGetUsers } from "@/pages/customers/hooks/useUsersData"; +import type { User } from "@/pages/customers/types/Types"; +import { type FC, useMemo, useState } from "react"; +import { toast } from "react-toastify"; +import { useAddUsersToGroup } from "../hooks/useUserGroupData"; + +type Props = { + open: boolean; + onClose: () => void; + groupId: string; + existingUserIds: string[]; +}; + +const AddUsersModal: FC = ({ open, onClose, groupId, existingUserIds }) => { + const [search, setSearch] = useState(""); + const [selectedUserIds, setSelectedUserIds] = useState([]); + + const { data: usersData, isLoading } = useGetUsers({ + search: search || undefined, + limit: 50, + }); + + const { mutate: addUsersToGroup, isPending } = useAddUsersToGroup(); + + const availableUsers = useMemo(() => { + const users = + usersData?.data?.map((userRestaurant) => userRestaurant.user) ?? []; + return users.filter((user) => !existingUserIds.includes(user.id)); + }, [usersData?.data, existingUserIds]); + + const handleToggle = (userId: string, checked: boolean) => { + if (checked) { + setSelectedUserIds((prev) => [...prev, userId]); + } else { + setSelectedUserIds((prev) => prev.filter((id) => id !== userId)); + } + }; + + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedUserIds(availableUsers.map((user) => user.id)); + } else { + setSelectedUserIds([]); + } + }; + + const isAllSelected = useMemo(() => { + return ( + availableUsers.length > 0 && + availableUsers.every((user) => selectedUserIds.includes(user.id)) + ); + }, [availableUsers, selectedUserIds]); + + const handleClose = () => { + setSearch(""); + setSelectedUserIds([]); + onClose(); + }; + + const handleSubmit = () => { + if (selectedUserIds.length === 0) { + toast.error("حداقل یک کاربر باید انتخاب شود"); + return; + } + + addUsersToGroup( + { groupId, params: { userIds: selectedUserIds } }, + { + onSuccess: () => { + toast.success("کاربران با موفقیت به گروه اضافه شدند"); + handleClose(); + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)); + }, + }, + ); + }; + + const getUserLabel = (user: User) => { + const fullName = [user.firstName, user.lastName].filter(Boolean).join(" ").trim(); + return fullName ? `${fullName} - ${user.phone}` : user.phone; + }; + + return ( + +
+ setSearch(e.target.value)} + /> + +
+ {isLoading ? ( +
در حال بارگذاری...
+ ) : availableUsers.length === 0 ? ( +
کاربری یافت نشد
+ ) : ( +
+
+ + +
+ {availableUsers.map((user) => ( +
+ + handleToggle(user.id, checked as boolean) + } + /> + +
+ ))} +
+ )} +
+ +
+ +
+
+
+ ); +}; + +export default AddUsersModal; diff --git a/src/pages/customers/groups/components/GroupUserTableColumns.tsx b/src/pages/customers/groups/components/GroupUserTableColumns.tsx new file mode 100644 index 0000000..4843a83 --- /dev/null +++ b/src/pages/customers/groups/components/GroupUserTableColumns.tsx @@ -0,0 +1,40 @@ +import type { ColumnType } from "@/components/types/TableTypes"; +import TrashWithConfrim from "@/components/TrashWithConfrim"; +import type { User } from "@/pages/customers/types/Types"; + +interface GetGroupUserTableColumnsParams { + onDelete?: (userId: string) => void; + isDeleting?: boolean; +} + +export const getGroupUserTableColumns = ({ + onDelete, + isDeleting, +}: GetGroupUserTableColumnsParams = {}): ColumnType[] => { + return [ + { + key: "fullName", + title: "نام و نام خانوادگی", + render: (item: User) => { + const fullName = [item.firstName, item.lastName].filter(Boolean).join(" ").trim(); + return fullName || "بدون نام"; + }, + }, + { + key: "phone", + title: "شماره تماس", + render: (item: User) => item.phone || "-", + }, + { + key: "actions", + title: "", + render: (item: User) => + onDelete ? ( + onDelete(item.id)} + isloading={isDeleting} + /> + ) : null, + }, + ]; +}; diff --git a/src/pages/customers/groups/components/GroupUsersSection.tsx b/src/pages/customers/groups/components/GroupUsersSection.tsx new file mode 100644 index 0000000..d08e3bd --- /dev/null +++ b/src/pages/customers/groups/components/GroupUsersSection.tsx @@ -0,0 +1,72 @@ +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import { extractErrorMessage } from "@/config/func"; +import type { ErrorType } from "@/helpers/types"; +import { Add } from "iconsax-react"; +import { type FC, useMemo, useState } from "react"; +import { toast } from "react-toastify"; +import { + useGetGroupUsers, + useRemoveUserFromGroup, +} from "../hooks/useUserGroupData"; +import AddUsersModal from "./AddUsersModal"; +import { getGroupUserTableColumns } from "./GroupUserTableColumns"; + +type Props = { + groupId: string; +}; + +const GroupUsersSection: FC = ({ groupId }) => { + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + + const { data: groupUsersData, isLoading } = useGetGroupUsers(groupId); + const { mutate: removeUserFromGroup, isPending: isRemoving } = + useRemoveUserFromGroup(); + + const users = groupUsersData?.data ?? []; + const existingUserIds = useMemo(() => users.map((user) => user.id), [users]); + + const columns = getGroupUserTableColumns({ + onDelete: (userId) => { + removeUserFromGroup( + { groupId, userId }, + { + onSuccess: () => { + toast.success("کاربر از گروه حذف شد"); + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)); + }, + }, + ); + }, + isDeleting: isRemoving, + }); + + return ( +
+
+
+
کاربران گروه
+ +
+ + + + + setIsAddModalOpen(false)} + groupId={groupId} + existingUserIds={existingUserIds} + /> + + ); +}; + +export default GroupUsersSection; diff --git a/src/pages/customers/groups/hooks/useUserGroupData.ts b/src/pages/customers/groups/hooks/useUserGroupData.ts index 638f635..00f3e9b 100644 --- a/src/pages/customers/groups/hooks/useUserGroupData.ts +++ b/src/pages/customers/groups/hooks/useUserGroupData.ts @@ -1,6 +1,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as api from "../service/UserGroupService"; -import type { CreateUserGroupType, GetUserGroupsParams } from "../types/Types"; +import type { + AddUsersToGroupType, + CreateUserGroupType, + GetUserGroupsParams, +} from "../types/Types"; export const useGetUserGroups = (params?: GetUserGroupsParams) => { return useQuery({ @@ -48,3 +52,47 @@ export const useUpdateUserGroup = () => { }, }); }; + +export const useGetGroupUsers = (groupId: string) => { + return useQuery({ + queryKey: ["user-group-users", groupId], + queryFn: () => api.getGroupUsers(groupId), + enabled: !!groupId, + }); +}; + +export const useAddUsersToGroup = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ + groupId, + params, + }: { + groupId: string; + params: AddUsersToGroupType; + }) => api.addUsersToGroup(groupId, params), + onSuccess: (_, { groupId }) => { + queryClient.invalidateQueries({ + queryKey: ["user-group-users", groupId], + }); + }, + }); +}; + +export const useRemoveUserFromGroup = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ + groupId, + userId, + }: { + groupId: string; + userId: string; + }) => api.removeUserFromGroup(groupId, userId), + onSuccess: (_, { groupId }) => { + queryClient.invalidateQueries({ + queryKey: ["user-group-users", groupId], + }); + }, + }); +}; diff --git a/src/pages/customers/groups/service/UserGroupService.ts b/src/pages/customers/groups/service/UserGroupService.ts index b39d8f3..de7d7b3 100644 --- a/src/pages/customers/groups/service/UserGroupService.ts +++ b/src/pages/customers/groups/service/UserGroupService.ts @@ -1,9 +1,11 @@ import axios from "@/config/axios"; import type { + AddUsersToGroupType, CreateUserGroupType, GetUserGroupsParams, UserGroupResponse, UserGroupsResponse, + UsersInGroupResponse, } from "../types/Types"; export const getUserGroups = async ( @@ -39,3 +41,30 @@ export const updateUserGroup = async ( const { data } = await axios.patch(`/admin/user-groups/${id}`, params); return data; }; + +export const getGroupUsers = async ( + groupId: string +): Promise => { + const { data } = await axios.get( + `/admin/user-groups/${groupId}/users` + ); + return data; +}; + +export const addUsersToGroup = async ( + groupId: string, + params: AddUsersToGroupType +) => { + const { data } = await axios.post( + `/admin/user-groups/${groupId}/users`, + params + ); + return data; +}; + +export const removeUserFromGroup = async (groupId: string, userId: string) => { + const { data } = await axios.delete( + `/admin/user-groups/${groupId}/users/${userId}` + ); + return data; +}; diff --git a/src/pages/customers/groups/types/Types.ts b/src/pages/customers/groups/types/Types.ts index b74a1b2..b01f598 100644 --- a/src/pages/customers/groups/types/Types.ts +++ b/src/pages/customers/groups/types/Types.ts @@ -1,3 +1,4 @@ +import type { User } from "@/pages/customers/types/Types"; import type { IResponse } from "@/types/response.types"; export type UserGroup = { @@ -20,3 +21,9 @@ export type GetUserGroupsParams = { limit?: number; search?: string; }; + +export type UsersInGroupResponse = IResponse; + +export type AddUsersToGroupType = { + userIds: string[]; +};