group
This commit is contained in:
@@ -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 = () => {
|
||||
<Input label="نام گروه" name="name" value={formik.values.name} onChange={formik.handleChange} error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{id && <GroupUsersSection groupId={id} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<Props> = ({ open, onClose, groupId, existingUserIds }) => {
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||
|
||||
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 (
|
||||
<DefaulModal
|
||||
open={open}
|
||||
close={handleClose}
|
||||
isHeader
|
||||
title_header="افزودن کاربر به گروه"
|
||||
width={520}
|
||||
>
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
<Input
|
||||
variant="search"
|
||||
className="bg-[#EEF0F7]"
|
||||
placeholder="جستجو با نام یا شماره تماس"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="border border-border rounded-xl p-4 max-h-72 overflow-y-auto">
|
||||
{isLoading ? (
|
||||
<div className="text-sm text-gray-500 text-center py-4">در حال بارگذاری...</div>
|
||||
) : availableUsers.length === 0 ? (
|
||||
<div className="text-sm text-gray-500 text-center py-4">کاربری یافت نشد</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} />
|
||||
<label className="text-sm cursor-pointer">همه</label>
|
||||
</div>
|
||||
{availableUsers.map((user) => (
|
||||
<div key={user.id} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedUserIds.includes(user.id)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleToggle(user.id, checked as boolean)
|
||||
}
|
||||
/>
|
||||
<label className="text-sm cursor-pointer">{getUserLabel(user)}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button className="px-6 w-fit" onClick={handleSubmit} isloading={isPending}>
|
||||
افزودن کاربران
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUsersModal;
|
||||
@@ -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<User>[] => {
|
||||
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 ? (
|
||||
<TrashWithConfrim
|
||||
onDelete={() => onDelete(item.id)}
|
||||
isloading={isDeleting}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -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<Props> = ({ 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 (
|
||||
<div className="mt-6">
|
||||
<div className="bg-white rounded-3xl p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="text-sm">کاربران گروه</div>
|
||||
<Button className="w-fit px-5" onClick={() => setIsAddModalOpen(true)}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Add color="#fff" size={18} />
|
||||
<span>افزودن کاربر</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table columns={columns} data={users} isloading={isLoading} />
|
||||
</div>
|
||||
|
||||
<AddUsersModal
|
||||
open={isAddModalOpen}
|
||||
onClose={() => setIsAddModalOpen(false)}
|
||||
groupId={groupId}
|
||||
existingUserIds={existingUserIds}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupUsersSection;
|
||||
@@ -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],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<UsersInGroupResponse> => {
|
||||
const { data } = await axios.get<UsersInGroupResponse>(
|
||||
`/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;
|
||||
};
|
||||
|
||||
@@ -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<User[]>;
|
||||
|
||||
export type AddUsersToGroupType = {
|
||||
userIds: string[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user