This commit is contained in:
hamid zarghami
2026-07-04 10:43:16 +03:30
parent 457361e759
commit a4eaa50fdf
7 changed files with 344 additions and 1 deletions
+3
View File
@@ -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[];
};