From 457361e75900faaed5b1f0712cb8212ab42dc74c Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 4 Jul 2026 10:34:56 +0330 Subject: [PATCH] Crud group --- src/config/Pages.ts | 3 + src/langs/fa.json | 3 +- src/pages/customers/Groups.tsx | 1 + src/pages/customers/groups/Create.tsx | 60 ++++++++++++ src/pages/customers/groups/List.tsx | 75 +++++++++++++++ src/pages/customers/groups/Update.tsx | 91 +++++++++++++++++++ .../components/UserGroupFiltersFields.tsx | 15 +++ .../components/UserGroupTableColumns.tsx | 47 ++++++++++ .../groups/hooks/useUserGroupData.ts | 50 ++++++++++ .../groups/hooks/useUserGroupFilters.ts | 42 +++++++++ .../groups/service/UserGroupService.ts | 41 +++++++++ src/pages/customers/groups/types/Types.ts | 22 +++++ src/router/Main.tsx | 6 ++ src/shared/components/CustomerSubMenu.tsx | 62 +++++-------- src/shared/components/CustomersSubMenu.tsx | 66 ++++++-------- 15 files changed, 504 insertions(+), 80 deletions(-) create mode 100644 src/pages/customers/Groups.tsx create mode 100644 src/pages/customers/groups/Create.tsx create mode 100644 src/pages/customers/groups/List.tsx create mode 100644 src/pages/customers/groups/Update.tsx create mode 100644 src/pages/customers/groups/components/UserGroupFiltersFields.tsx create mode 100644 src/pages/customers/groups/components/UserGroupTableColumns.tsx create mode 100644 src/pages/customers/groups/hooks/useUserGroupData.ts create mode 100644 src/pages/customers/groups/hooks/useUserGroupFilters.ts create mode 100644 src/pages/customers/groups/service/UserGroupService.ts create mode 100644 src/pages/customers/groups/types/Types.ts diff --git a/src/config/Pages.ts b/src/config/Pages.ts index b1c4ef1..36b2209 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -33,6 +33,9 @@ export const Pages = { add: "/customers/add", detail: "/customers/detail/", update: "/customers/update/", + groups: "/customers/groups", + groupsAdd: "/customers/groups/add", + groupsUpdate: "/customers/groups/update/", }, discount: { list: "/discounts/list", diff --git a/src/langs/fa.json b/src/langs/fa.json index d9a4c34..352d2bd 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -104,7 +104,8 @@ "discount_list": "لیست تخفیف", "add_discount": "افزودن تخفیف", "customers_list": "لیست مشتریان", - "add_customer": "افزودن مشتری" + "add_customer": "افزودن مشتری", + "groups": "گروه مشتری" }, "guide": { "list_guide": "لیست راهنمای سرویس", diff --git a/src/pages/customers/Groups.tsx b/src/pages/customers/Groups.tsx new file mode 100644 index 0000000..316882f --- /dev/null +++ b/src/pages/customers/Groups.tsx @@ -0,0 +1 @@ +export { default } from "./groups/List"; diff --git a/src/pages/customers/groups/Create.tsx b/src/pages/customers/groups/Create.tsx new file mode 100644 index 0000000..63c7858 --- /dev/null +++ b/src/pages/customers/groups/Create.tsx @@ -0,0 +1,60 @@ +import Button from "@/components/Button"; +import Input from "@/components/Input"; +import { extractErrorMessage } from "@/config/func"; +import { Pages } from "@/config/Pages"; +import type { ErrorType } from "@/helpers/types"; +import { useFormik } from "formik"; +import { TickCircle } from "iconsax-react"; +import { type FC } from "react"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import * as Yup from "yup"; +import { useCreateUserGroup } from "./hooks/useUserGroupData"; +import type { CreateUserGroupType } from "./types/Types"; + +const CreateUserGroup: FC = () => { + const navigate = useNavigate(); + const { mutate: createUserGroup, isPending } = useCreateUserGroup(); + + const formik = useFormik({ + initialValues: { + name: "", + }, + validationSchema: Yup.object().shape({ + name: Yup.string().required("نام گروه الزامی است"), + }), + onSubmit: (values) => { + createUserGroup(values, { + onSuccess: () => { + toast.success("گروه با موفقیت ایجاد شد"); + navigate(Pages.customers.groups); + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)); + }, + }); + }, + }); + + return ( +
+
+
افزودن گروه مشتری
+ +
+ +
+
+ +
+
+
+ ); +}; + +export default CreateUserGroup; diff --git a/src/pages/customers/groups/List.tsx b/src/pages/customers/groups/List.tsx new file mode 100644 index 0000000..823413d --- /dev/null +++ b/src/pages/customers/groups/List.tsx @@ -0,0 +1,75 @@ +import { type FC } from "react"; +import Table from "@/components/Table"; +import Filters from "@/components/Filters"; +import { Add } from "iconsax-react"; +import Button from "@/components/Button"; +import { Link } from "react-router-dom"; +import { Pages } from "@/config/Pages"; +import { + useDeleteUserGroup, + useGetUserGroups, +} from "./hooks/useUserGroupData"; +import { useUserGroupFilters } from "./hooks/useUserGroupFilters"; +import { getUserGroupTableColumns } from "./components/UserGroupTableColumns"; +import { useUserGroupFiltersFields } from "./components/UserGroupFiltersFields"; + +const UserGroupsList: FC = () => { + const { + filters, + currentPage, + apiParams, + handleFiltersChange, + handlePageChange, + limit, + } = useUserGroupFilters(); + + const { data: groupsData, isLoading } = useGetUserGroups(apiParams); + const { mutate: deleteUserGroup, isPending: isDeleting } = + useDeleteUserGroup(); + + const groups = groupsData?.data || []; + const columns = getUserGroupTableColumns({ + onDelete: (id: string) => deleteUserGroup(id), + isDeleting, + }); + const filterFields = useUserGroupFiltersFields(); + const totalPages = Math.ceil(groups.length / limit) || 1; + + return ( +
+
+

گروه‌های مشتری

+ + + +
+ +
+ +
+ + + + ); +}; + +export default UserGroupsList; diff --git a/src/pages/customers/groups/Update.tsx b/src/pages/customers/groups/Update.tsx new file mode 100644 index 0000000..6145136 --- /dev/null +++ b/src/pages/customers/groups/Update.tsx @@ -0,0 +1,91 @@ +import Button from "@/components/Button"; +import Input from "@/components/Input"; +import PageLoading from "@/components/PageLoading"; +import { extractErrorMessage } from "@/config/func"; +import { Pages } from "@/config/Pages"; +import type { ErrorType } from "@/helpers/types"; +import { useFormik } from "formik"; +import { TickCircle } from "iconsax-react"; +import { type FC, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { toast } from "react-toastify"; +import * as Yup from "yup"; +import { useGetUserGroup, useUpdateUserGroup } from "./hooks/useUserGroupData"; +import type { CreateUserGroupType } from "./types/Types"; + +const UpdateUserGroup: FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const { data: groupData, isLoading } = useGetUserGroup(id || ""); + const { mutate: updateUserGroup, isPending } = useUpdateUserGroup(); + + const group = groupData?.data; + + const formik = useFormik({ + initialValues: { + name: "", + }, + validationSchema: Yup.object().shape({ + name: Yup.string().required("نام گروه الزامی است"), + }), + onSubmit: (values) => { + if (!id) return; + + updateUserGroup( + { id, params: values }, + { + onSuccess: () => { + toast.success("گروه با موفقیت ویرایش شد"); + navigate(Pages.customers.groups); + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)); + }, + }, + ); + }, + }); + + useEffect(() => { + if (group) { + formik.setValues({ + name: group.name || "", + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [group]); + + if (isLoading) { + return ; + } + + if (!group) { + return ( +
+
گروه یافت نشد
+
+ ); + } + + return ( +
+
+
ویرایش گروه مشتری
+ +
+ +
+
+ +
+
+
+ ); +}; + +export default UpdateUserGroup; diff --git a/src/pages/customers/groups/components/UserGroupFiltersFields.tsx b/src/pages/customers/groups/components/UserGroupFiltersFields.tsx new file mode 100644 index 0000000..1ed6b44 --- /dev/null +++ b/src/pages/customers/groups/components/UserGroupFiltersFields.tsx @@ -0,0 +1,15 @@ +import { useMemo } from "react"; +import type { FieldType } from "@/components/Filters"; + +export const useUserGroupFiltersFields = (): FieldType[] => { + return useMemo( + () => [ + { + type: "input", + name: "search", + placeholder: "جستجو", + }, + ], + [] + ); +}; diff --git a/src/pages/customers/groups/components/UserGroupTableColumns.tsx b/src/pages/customers/groups/components/UserGroupTableColumns.tsx new file mode 100644 index 0000000..4eddc25 --- /dev/null +++ b/src/pages/customers/groups/components/UserGroupTableColumns.tsx @@ -0,0 +1,47 @@ +import type { ColumnType } from "@/components/types/TableTypes"; +import type { UserGroup } from "../types/Types"; +import TrashWithConfrim from "@/components/TrashWithConfrim"; +import { Eye } from "iconsax-react"; +import { Link } from "react-router-dom"; +import { Pages } from "@/config/Pages"; +import { formatOptionalDate } from "@/helpers/func"; + +interface GetUserGroupTableColumnsParams { + onDelete?: (id: string) => void; + isDeleting?: boolean; +} + +export const getUserGroupTableColumns = ({ + onDelete, + isDeleting, +}: GetUserGroupTableColumnsParams = {}): ColumnType[] => { + return [ + { + key: "name", + title: "نام گروه", + render: (item: UserGroup) => item.name || "-", + }, + { + key: "createdAt", + title: "تاریخ ایجاد", + render: (item: UserGroup) => formatOptionalDate(item.createdAt), + }, + { + key: "actions", + title: "", + render: (item: UserGroup) => ( +
+ + + + {onDelete && ( + onDelete(item.id)} + isloading={isDeleting} + /> + )} +
+ ), + }, + ]; +}; diff --git a/src/pages/customers/groups/hooks/useUserGroupData.ts b/src/pages/customers/groups/hooks/useUserGroupData.ts new file mode 100644 index 0000000..638f635 --- /dev/null +++ b/src/pages/customers/groups/hooks/useUserGroupData.ts @@ -0,0 +1,50 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import * as api from "../service/UserGroupService"; +import type { CreateUserGroupType, GetUserGroupsParams } from "../types/Types"; + +export const useGetUserGroups = (params?: GetUserGroupsParams) => { + return useQuery({ + queryKey: ["user-groups", params], + queryFn: () => api.getUserGroups(params), + }); +}; + +export const useCreateUserGroup = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: api.createUserGroup, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user-groups"] }); + }, + }); +}; + +export const useDeleteUserGroup = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: api.deleteUserGroup, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user-groups"] }); + }, + }); +}; + +export const useGetUserGroup = (id: string) => { + return useQuery({ + queryKey: ["user-group", id], + queryFn: () => api.getUserGroup(id), + enabled: !!id, + }); +}; + +export const useUpdateUserGroup = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, params }: { id: string; params: CreateUserGroupType }) => + api.updateUserGroup(id, params), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ["user-groups"] }); + queryClient.invalidateQueries({ queryKey: ["user-group", id] }); + }, + }); +}; diff --git a/src/pages/customers/groups/hooks/useUserGroupFilters.ts b/src/pages/customers/groups/hooks/useUserGroupFilters.ts new file mode 100644 index 0000000..2b5deb5 --- /dev/null +++ b/src/pages/customers/groups/hooks/useUserGroupFilters.ts @@ -0,0 +1,42 @@ +import { useMemo, useState } from "react"; +import type { FilterValues } from "@/components/Filters"; + +const DEFAULT_PAGE = 1; +const DEFAULT_LIMIT = 10; + +export const useUserGroupFilters = () => { + const [filters, setFilters] = useState({}); + const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE); + const [limit] = useState(DEFAULT_LIMIT); + + const apiParams = useMemo(() => { + const params: Record = { + page: currentPage, + limit, + }; + + if (filters.search) { + params.search = filters.search; + } + + return params; + }, [filters, currentPage, limit]); + + const handleFiltersChange = (newFilters: FilterValues) => { + setFilters(newFilters); + setCurrentPage(DEFAULT_PAGE); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + return { + filters, + currentPage, + apiParams, + handleFiltersChange, + handlePageChange, + limit, + }; +}; diff --git a/src/pages/customers/groups/service/UserGroupService.ts b/src/pages/customers/groups/service/UserGroupService.ts new file mode 100644 index 0000000..b39d8f3 --- /dev/null +++ b/src/pages/customers/groups/service/UserGroupService.ts @@ -0,0 +1,41 @@ +import axios from "@/config/axios"; +import type { + CreateUserGroupType, + GetUserGroupsParams, + UserGroupResponse, + UserGroupsResponse, +} from "../types/Types"; + +export const getUserGroups = async ( + params?: GetUserGroupsParams +): Promise => { + const { data } = await axios.get("/admin/user-groups", { + params, + }); + return data; +}; + +export const createUserGroup = async (params: CreateUserGroupType) => { + const { data } = await axios.post("/admin/user-groups", params); + return data; +}; + +export const deleteUserGroup = async (id: string) => { + const { data } = await axios.delete(`/admin/user-groups/${id}`); + return data; +}; + +export const getUserGroup = async (id: string): Promise => { + const { data } = await axios.get( + `/admin/user-groups/${id}` + ); + return data; +}; + +export const updateUserGroup = async ( + id: string, + params: CreateUserGroupType +) => { + const { data } = await axios.patch(`/admin/user-groups/${id}`, params); + return data; +}; diff --git a/src/pages/customers/groups/types/Types.ts b/src/pages/customers/groups/types/Types.ts new file mode 100644 index 0000000..b74a1b2 --- /dev/null +++ b/src/pages/customers/groups/types/Types.ts @@ -0,0 +1,22 @@ +import type { IResponse } from "@/types/response.types"; + +export type UserGroup = { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + name: string; +}; + +export type UserGroupsResponse = IResponse; +export type UserGroupResponse = IResponse; + +export type CreateUserGroupType = { + name: string; +}; + +export type GetUserGroupsParams = { + page?: number; + limit?: number; + search?: string; +}; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index ce23fca..1b06ce3 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -50,6 +50,9 @@ import Statistics from '@/pages/statistics/Statistics' import SlidersList from '@/pages/sliders/List' import CreateSlider from '@/pages/sliders/Create' import UpdateSlider from '@/pages/sliders/Update' +import UserGroupsList from '@/pages/customers/groups/List' +import CreateUserGroup from '@/pages/customers/groups/Create' +import UpdateUserGroup from '@/pages/customers/groups/Update' const MainRouter: FC = () => { const { hasSubMenu } = useSharedStore() @@ -78,6 +81,9 @@ const MainRouter: FC = () => { } /> } /> } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/shared/components/CustomerSubMenu.tsx b/src/shared/components/CustomerSubMenu.tsx index 662d70a..2b0a371 100644 --- a/src/shared/components/CustomerSubMenu.tsx +++ b/src/shared/components/CustomerSubMenu.tsx @@ -1,45 +1,29 @@ -import { People } from 'iconsax-react' -import { type FC } from 'react' -import { useTranslation } from 'react-i18next' -import SubMenuItem from './SubMenuItem' -import { Pages } from '../../config/Pages' +import { People } from "iconsax-react"; +import { type FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Pages } from "../../config/Pages"; +import SubMenuItem from "./SubMenuItem"; const CustomerSubMenu: FC = () => { + const { t } = useTranslation("global"); - const { t } = useTranslation('global') + const isActive = (name: string) => { + return location.pathname.includes(name); + }; - const isActive = (name: string) => { - return location.pathname.includes(name) - } + return ( +
+
+ +
{t("sidebar.customers")}
+
- return ( -
-
- -
- {t('sidebar.customers')} -
-
+
+ + +
+
+ ); +}; -
- - -
- -
- ) -} - -export default CustomerSubMenu \ No newline at end of file +export default CustomerSubMenu; diff --git a/src/shared/components/CustomersSubMenu.tsx b/src/shared/components/CustomersSubMenu.tsx index 0165090..e4c1afe 100644 --- a/src/shared/components/CustomersSubMenu.tsx +++ b/src/shared/components/CustomersSubMenu.tsx @@ -1,47 +1,33 @@ -import { People } from 'iconsax-react' -import { type FC } from 'react' -import { useTranslation } from 'react-i18next' -import SubMenuItem from './SubMenuItem' -import { useLocation } from 'react-router-dom' -import { Pages } from '../../config/Pages' +import { People } from "iconsax-react"; +import { type FC } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router-dom"; +import { Pages } from "../../config/Pages"; +import SubMenuItem from "./SubMenuItem"; const CustomersSubMenu: FC = () => { + const location = useLocation(); + const { t } = useTranslation("global"); - const location = useLocation() - const { t } = useTranslation('global') + const isActive = (name: string) => { + return location.pathname.includes(name); + }; - const isActive = (name: string) => { - return location.pathname.includes(name) - } + return ( +
+
+ +
{t("sidebar.customers")}
+
- return ( -
-
- -
- {t('sidebar.customers')} -
-
+
+ + -
- - -
-
- ) -} - -export default CustomersSubMenu + +
+
+ ); +}; +export default CustomersSubMenu;