group
This commit is contained in:
@@ -12,6 +12,7 @@ import { toast } from "react-toastify";
|
|||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { useGetUserGroup, useUpdateUserGroup } from "./hooks/useUserGroupData";
|
import { useGetUserGroup, useUpdateUserGroup } from "./hooks/useUserGroupData";
|
||||||
import type { CreateUserGroupType } from "./types/Types";
|
import type { CreateUserGroupType } from "./types/Types";
|
||||||
|
import GroupUsersSection from "./components/GroupUsersSection";
|
||||||
|
|
||||||
const UpdateUserGroup: FC = () => {
|
const UpdateUserGroup: FC = () => {
|
||||||
const navigate = useNavigate();
|
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 : ""} />
|
<Input label="نام گروه" name="name" value={formik.values.name} onChange={formik.handleChange} error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ""} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{id && <GroupUsersSection groupId={id} />}
|
||||||
</div>
|
</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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/UserGroupService";
|
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) => {
|
export const useGetUserGroups = (params?: GetUserGroupsParams) => {
|
||||||
return useQuery({
|
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 axios from "@/config/axios";
|
||||||
import type {
|
import type {
|
||||||
|
AddUsersToGroupType,
|
||||||
CreateUserGroupType,
|
CreateUserGroupType,
|
||||||
GetUserGroupsParams,
|
GetUserGroupsParams,
|
||||||
UserGroupResponse,
|
UserGroupResponse,
|
||||||
UserGroupsResponse,
|
UserGroupsResponse,
|
||||||
|
UsersInGroupResponse,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const getUserGroups = async (
|
export const getUserGroups = async (
|
||||||
@@ -39,3 +41,30 @@ export const updateUserGroup = async (
|
|||||||
const { data } = await axios.patch(`/admin/user-groups/${id}`, params);
|
const { data } = await axios.patch(`/admin/user-groups/${id}`, params);
|
||||||
return data;
|
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";
|
import type { IResponse } from "@/types/response.types";
|
||||||
|
|
||||||
export type UserGroup = {
|
export type UserGroup = {
|
||||||
@@ -20,3 +21,9 @@ export type GetUserGroupsParams = {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UsersInGroupResponse = IResponse<User[]>;
|
||||||
|
|
||||||
|
export type AddUsersToGroupType = {
|
||||||
|
userIds: string[];
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user