Crud group

This commit is contained in:
hamid zarghami
2026-07-04 10:34:56 +03:30
parent eb9973678c
commit 457361e759
15 changed files with 504 additions and 80 deletions
+3
View File
@@ -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",
+2 -1
View File
@@ -104,7 +104,8 @@
"discount_list": "لیست تخفیف",
"add_discount": "افزودن تخفیف",
"customers_list": "لیست مشتریان",
"add_customer": "افزودن مشتری"
"add_customer": "افزودن مشتری",
"groups": "گروه مشتری"
},
"guide": {
"list_guide": "لیست راهنمای سرویس",
+1
View File
@@ -0,0 +1 @@
export { default } from "./groups/List";
+60
View File
@@ -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<CreateUserGroupType>({
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 (
<div className="mt-5">
<div className="flex w-full justify-between items-center">
<div className="text-lg font-light">افزودن گروه مشتری</div>
<Button className="px-5 w-fit" onClick={() => formik.handleSubmit()} isloading={isPending}>
<div className="flex gap-2 items-center">
<TickCircle className="size-5" color="white" />
<div>ثبت گروه</div>
</div>
</Button>
</div>
<div className="mt-6">
<div className="bg-white rounded-3xl p-6">
<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>
);
};
export default CreateUserGroup;
+75
View File
@@ -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 (
<div className="mt-5">
<div className="flex justify-between items-center">
<h1 className="text-lg font-light">گروههای مشتری</h1>
<Link to={Pages.customers.groupsAdd}>
<Button className="w-fit px-6">
<div className="flex gap-2 items-center">
<Add color="#fff" size={20} />
<span>افزودن گروه</span>
</div>
</Button>
</Link>
</div>
<div className="mt-8">
<Filters
fields={filterFields}
onChange={handleFiltersChange}
initialValues={filters}
searchField="search"
/>
</div>
<Table
columns={columns}
data={groups}
isloading={isLoading}
pagination={{
currentPage,
totalPages,
onPageChange: handlePageChange,
}}
/>
</div>
);
};
export default UserGroupsList;
+91
View File
@@ -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<CreateUserGroupType>({
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 <PageLoading />;
}
if (!group) {
return (
<div className="w-full mt-4 flex justify-center items-center">
<div>گروه یافت نشد</div>
</div>
);
}
return (
<div className="mt-5">
<div className="flex w-full justify-between items-center">
<div className="text-lg font-light">ویرایش گروه مشتری</div>
<Button className="px-5 w-fit" onClick={() => formik.handleSubmit()} isloading={isPending}>
<div className="flex gap-2 items-center">
<TickCircle className="size-5" color="white" />
<div>ثبت تغییرات</div>
</div>
</Button>
</div>
<div className="mt-6">
<div className="bg-white rounded-3xl p-6">
<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>
);
};
export default UpdateUserGroup;
@@ -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: "جستجو",
},
],
[]
);
};
@@ -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<UserGroup>[] => {
return [
{
key: "name",
title: "نام گروه",
render: (item: UserGroup) => item.name || "-",
},
{
key: "createdAt",
title: "تاریخ ایجاد",
render: (item: UserGroup) => formatOptionalDate(item.createdAt),
},
{
key: "actions",
title: "",
render: (item: UserGroup) => (
<div className="flex gap-2 items-center">
<Link to={Pages.customers.groupsUpdate + item.id}>
<Eye size={20} color="#8C90A3" className="cursor-pointer" />
</Link>
{onDelete && (
<TrashWithConfrim
onDelete={() => onDelete(item.id)}
isloading={isDeleting}
/>
)}
</div>
),
},
];
};
@@ -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] });
},
});
};
@@ -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<FilterValues>({});
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
const [limit] = useState(DEFAULT_LIMIT);
const apiParams = useMemo(() => {
const params: Record<string, unknown> = {
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,
};
};
@@ -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<UserGroupsResponse> => {
const { data } = await axios.get<UserGroupsResponse>("/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<UserGroupResponse> => {
const { data } = await axios.get<UserGroupResponse>(
`/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;
};
+22
View File
@@ -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<UserGroup[]>;
export type UserGroupResponse = IResponse<UserGroup>;
export type CreateUserGroupType = {
name: string;
};
export type GetUserGroupsParams = {
page?: number;
limit?: number;
search?: string;
};
+6
View File
@@ -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 = () => {
<Route path={Pages.customers.detail + ':id'} element={<CustomerDetails />} />
<Route path={Pages.customers.add} element={<CreateCustomer />} />
<Route path={Pages.customers.update + ':id'} element={<UpdateCustomer />} />
<Route path={Pages.customers.groups} element={<UserGroupsList />} />
<Route path={Pages.customers.groupsAdd} element={<CreateUserGroup />} />
<Route path={Pages.customers.groupsUpdate + ':id'} element={<UpdateUserGroup />} />
<Route path={Pages.orders.list} element={<OrdersList />} />
<Route path={Pages.orders.add} element={<CreateOrder />} />
<Route path={Pages.orders.foodReport} element={<FoodOrderReport />} />
+23 -39
View File
@@ -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 (
<div className="py-12">
<div className="flex gap-3 items-center border-b border-border pb-10 px-6">
<People size={24} color="#000" variant="Bold" />
<div className="text-xs">{t("sidebar.customers")}</div>
</div>
return (
<div className='py-12'>
<div className='flex gap-3 items-center border-b border-border pb-10 px-6'>
<People
size={24}
color='#000'
variant='Bold'
/>
<div className='text-xs'>
{t('sidebar.customers')}
</div>
</div>
<div className="flex flex-col mt-10 gap-4">
<SubMenuItem title={t("submenu.customer_list")} isActive={isActive("list")} link={Pages.orders.list} />
<SubMenuItem title={t("submenu.create_customer")} isActive={isActive("create")} link={Pages.orders.add} />
</div>
</div>
);
};
<div className='flex flex-col mt-10 gap-4'>
<SubMenuItem
title={t('submenu.customer_list')}
isActive={isActive('list')}
link={Pages.orders.list}
/>
<SubMenuItem
title={t('submenu.create_customer')}
isActive={isActive('create')}
link={Pages.orders.add}
/>
</div>
</div>
)
}
export default CustomerSubMenu
export default CustomerSubMenu;
+26 -40
View File
@@ -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 (
<div className="py-12">
<div className="flex gap-3 items-center border-b border-border pb-10 px-6">
<People size={24} color="#000" variant="Bold" />
<div className="text-xs">{t("sidebar.customers")}</div>
</div>
return (
<div className='py-12'>
<div className='flex gap-3 items-center border-b border-border pb-10 px-6'>
<People
size={24}
color='#000'
variant='Bold'
/>
<div className='text-xs'>
{t('sidebar.customers')}
</div>
</div>
<div className="flex flex-col mt-10 gap-4">
<SubMenuItem title={t("submenu.customers_list")} isActive={isActive("list")} link={Pages.customers.list} />
<SubMenuItem title={t("submenu.add_customer")} isActive={isActive("add")} link={Pages.customers.add} />
<div className='flex flex-col mt-10 gap-4'>
<SubMenuItem
title={t('submenu.customers_list')}
isActive={isActive('list')}
link={Pages.customers.list}
/>
<SubMenuItem
title={t('submenu.add_customer')}
isActive={isActive('add')}
link={Pages.customers.add}
/>
</div>
</div>
)
}
export default CustomersSubMenu
<SubMenuItem title={t("submenu.groups")} isActive={isActive("groups")} link={Pages.customers.groups} />
</div>
</div>
);
};
export default CustomersSubMenu;