crud role

This commit is contained in:
hamid zarghami
2026-02-26 09:24:41 +03:30
parent 54b86b718c
commit f466f03fea
9 changed files with 577 additions and 0 deletions
+5
View File
@@ -31,6 +31,11 @@ export const Paths = {
create: '/admin/create',
update: '/admin/update/',
},
role: {
list: '/admin/role',
create: '/admin/role/create',
update: '/admin/role/update/',
},
formBuilder: {
list: '/form-builder/list/',
create: '/form-builder/create/',
+149
View File
@@ -0,0 +1,149 @@
import { useFormik } from "formik";
import { type FC } from "react";
import type { CreateRoleType } from "./types/Types";
import * as Yup from "yup";
import Button from "@/components/Button";
import { AddSquare } from "iconsax-react";
import Input from "@/components/Input";
import { useCreateRole, useGetPermissions } from "./hooks/useRoleData";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { extractErrorMessage } from "@/config/func";
import { Paths } from "@/config/Paths";
import { Checkbox } from "@/components/ui/checkbox";
import { clx } from "@/helpers/utils";
const CreateRole: FC = () => {
const navigate = useNavigate();
const { mutate: createRole, isPending } = useCreateRole();
const { data: permissionsData } = useGetPermissions();
const formik = useFormik<CreateRoleType>({
initialValues: {
name: "",
title: "",
permissionIds: [],
},
validationSchema: Yup.object({
name: Yup.string()
.required("نام اجباری می باشد")
.min(2, "نام باید حداقل 2 کاراکتر باشد"),
title: Yup.string(),
permissionIds: Yup.array().of(Yup.string()),
}),
onSubmit: (values) => {
createRole(values, {
onSuccess: () => {
toast.success("نقش با موفقیت ایجاد شد");
navigate(Paths.role.list);
},
onError: (error) => {
toast.error(extractErrorMessage(error));
},
});
},
});
const permissions = permissionsData?.data ?? [];
const togglePermission = (permissionId: string) => {
const current = formik.values.permissionIds;
const next = current.includes(permissionId)
? current.filter((id) => id !== permissionId)
: [...current, permissionId];
formik.setFieldValue("permissionIds", next);
};
const selectAll = () => {
if (formik.values.permissionIds.length === permissions.length) {
formik.setFieldValue("permissionIds", []);
} else {
formik.setFieldValue(
"permissionIds",
permissions.map((p) => p.id)
);
}
};
return (
<div className="mt-5">
<div className="flex justify-between items-center">
<h1 className="text-lg font-light">ایجاد نقش جدید</h1>
<Button
className="w-fit px-6"
onClick={() => formik.handleSubmit()}
isLoading={isPending}
>
<div className="flex gap-1.5">
<AddSquare size={18} color="black" />
<div className="text-[13px] font-light">ایجاد نقش</div>
</div>
</Button>
</div>
<div className="mt-8 bg-white p-6 rounded-3xl flex-1">
<div className="rowTwoInput">
<Input
label="نام"
placeholder="نام را وارد کنید"
{...formik.getFieldProps("name")}
error_text={
formik.touched.name && formik.errors.name ? formik.errors.name : ""
}
/>
<Input
label="عنوان"
placeholder="عنوان را وارد کنید"
{...formik.getFieldProps("title")}
error_text={
formik.touched.title && formik.errors.title
? formik.errors.title
: ""
}
/>
</div>
<div className="mt-6">
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700">
دسترسیها
</label>
<button
type="button"
onClick={selectAll}
className="text-xs text-primary hover:underline"
>
{formik.values.permissionIds.length === permissions.length
? "حذف همه"
: "انتخاب همه"}
</button>
</div>
<div
className={clx(
"border border-gray-200 rounded-xl p-4 max-h-64 overflow-y-auto",
"flex gap-6 flex-wrap"
)}
>
{permissions.map((p) => (
<label
key={p.id}
className="flex items-center gap-2 cursor-pointer hover:bg-gray-50 p-2 rounded-lg"
>
<Checkbox
checked={formik.values.permissionIds.includes(p.id)}
onCheckedChange={() => togglePermission(p.id)}
/>
<span className="text-sm">
{p.title || p.name}
</span>
</label>
))}
</div>
</div>
</div>
</div>
);
};
export default CreateRole;
+87
View File
@@ -0,0 +1,87 @@
import { type FC } from "react";
import { useGetRoles, useDeleteRole } from "./hooks/useRoleData";
import Table from "@/components/Table";
import moment from "moment-jalaali";
import { Link } from "react-router-dom";
import { Paths } from "@/config/Paths";
import Button from "@/components/Button";
import { AddSquare, Eye } from "iconsax-react";
import TrashWithConfrim from "@/components/TrashWithConfrim";
import type { RoleItemType } from "./types/Types";
const RoleList: FC = () => {
const { data, isLoading } = useGetRoles();
const deleteRoleMutation = useDeleteRole();
return (
<div className="mt-5">
<div className="flex justify-between items-center">
<h1 className="text-lg font-light">نقشها</h1>
<Link to={Paths.role.create}>
<Button className="w-fit px-6">
<div className="flex gap-1.5">
<AddSquare size={18} color="black" />
<div className="text-[13px] font-light">نقش جدید</div>
</div>
</Button>
</Link>
</div>
<Table<RoleItemType>
columns={[
{
key: "name",
title: "نام",
},
{
key: "title",
title: "عنوان",
},
{
key: "isSystem",
title: "سیستمی",
render: (item) => (
<div>{item.isSystem ? "بله" : "خیر"}</div>
),
},
{
key: "permissionsCount",
title: "تعداد دسترسی‌ها",
render: (item) => (
<div>{item.permissions?.length ?? 0}</div>
),
},
{
key: "createdAt",
title: "تاریخ ثبت",
render: (item) => (
<div>{moment(item.createdAt).format("jYYYY-jMM-jDD")}</div>
),
},
{
key: "actions",
title: "عملیات",
render: (item) => (
<div className="flex gap-2">
<Link to={`${Paths.role.update}${item.id}`}>
<Eye size={20} color="black" />
</Link>
{!item.isSystem && (
<TrashWithConfrim
onDelete={() => deleteRoleMutation.mutate(item.id)}
isloading={deleteRoleMutation.isPending}
colorIcon="#ef4444"
/>
)}
</div>
),
},
]}
data={data?.data}
isLoading={isLoading}
/>
</div>
);
};
export default RoleList;
+186
View File
@@ -0,0 +1,186 @@
import { useFormik } from "formik";
import { type FC, useEffect } from "react";
import type { CreateRoleType } from "./types/Types";
import * as Yup from "yup";
import Button from "@/components/Button";
import { Edit } from "iconsax-react";
import Input from "@/components/Input";
import { useGetRoleById, useGetPermissions, useUpdateRole } from "./hooks/useRoleData";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { extractErrorMessage } from "@/config/func";
import { Paths } from "@/config/Paths";
import { Checkbox } from "@/components/ui/checkbox";
import { clx } from "@/helpers/utils";
const UpdateRole: FC = () => {
const navigate = useNavigate();
const { id: roleId } = useParams<{ id: string }>();
const { mutate: update, isPending: isUpdating } = useUpdateRole();
const { data: permissionsData } = useGetPermissions();
const { data: roleData, isLoading: isLoadingRole } = useGetRoleById(roleId);
const formik = useFormik<CreateRoleType>({
initialValues: {
name: "",
title: "",
permissionIds: [],
},
validationSchema: Yup.object({
name: Yup.string()
.required("نام اجباری می باشد")
.min(2, "نام باید حداقل 2 کاراکتر باشد"),
title: Yup.string(),
permissionIds: Yup.array().of(Yup.string()),
}),
onSubmit: (values) => {
if (!roleId) return;
update(
{ roleId, params: values },
{
onSuccess: () => {
toast.success("نقش با موفقیت ویرایش شد");
navigate(Paths.role.list);
},
onError: (error) => {
toast.error(extractErrorMessage(error));
},
}
);
},
});
useEffect(() => {
if (roleData?.data) {
const role = roleData.data;
formik.setValues({
name: role.name ?? "",
title: role.title ?? "",
permissionIds: role.permissions?.map((p) => p.id) ?? [],
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [roleData]);
const permissions = permissionsData?.data ?? [];
const togglePermission = (permissionId: string) => {
const current = formik.values.permissionIds;
const next = current.includes(permissionId)
? current.filter((id) => id !== permissionId)
: [...current, permissionId];
formik.setFieldValue("permissionIds", next);
};
const selectAll = () => {
if (formik.values.permissionIds.length === permissions.length) {
formik.setFieldValue("permissionIds", []);
} else {
formik.setFieldValue(
"permissionIds",
permissions.map((p) => p.id)
);
}
};
if (isLoadingRole) {
return (
<div className="mt-5">
<div className="flex justify-center items-center h-64">
<div className="text-lg">در حال بارگذاری...</div>
</div>
</div>
);
}
if (!roleData?.data) {
return (
<div className="mt-5">
<div className="flex justify-center items-center h-64">
<div className="text-lg text-red-500">نقش یافت نشد</div>
</div>
</div>
);
}
return (
<div className="mt-5">
<div className="flex justify-between items-center">
<h1 className="text-lg font-light">ویرایش نقش</h1>
<Button
className="w-fit px-6"
onClick={() => formik.handleSubmit()}
isLoading={isUpdating}
>
<div className="flex gap-1.5">
<Edit size={18} color="black" />
<div className="text-[13px] font-light">ویرایش نقش</div>
</div>
</Button>
</div>
<div className="mt-8 bg-white p-6 rounded-3xl flex-1">
<div className="rowTwoInput">
<Input
label="نام"
placeholder="نام را وارد کنید"
{...formik.getFieldProps("name")}
error_text={
formik.touched.name && formik.errors.name ? formik.errors.name : ""
}
/>
<Input
label="عنوان"
placeholder="عنوان را وارد کنید"
{...formik.getFieldProps("title")}
error_text={
formik.touched.title && formik.errors.title
? formik.errors.title
: ""
}
/>
</div>
<div className="mt-6">
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700">
دسترسیها
</label>
<button
type="button"
onClick={selectAll}
className="text-xs text-primary hover:underline"
>
{formik.values.permissionIds.length === permissions.length
? "حذف همه"
: "انتخاب همه"}
</button>
</div>
<div
className={clx(
"border border-gray-200 rounded-xl p-4 max-h-64 overflow-y-auto",
"flex gap-6 flex-wrap"
)}
>
{permissions.map((p) => (
<label
key={p.id}
className="flex items-center gap-2 cursor-pointer hover:bg-gray-50 p-2 rounded-lg"
>
<Checkbox
checked={formik.values.permissionIds.includes(p.id)}
onCheckedChange={() => togglePermission(p.id)}
/>
<span className="text-sm">{p.title || p.name}</span>
</label>
))}
</div>
</div>
</div>
</div>
);
};
export default UpdateRole;
+64
View File
@@ -0,0 +1,64 @@
import * as api from "../service/RoleService";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CreateRoleType } from "../types/Types";
export const useGetRoles = () => {
return useQuery({
queryKey: ["roles"],
queryFn: api.getRoles,
});
};
export const useGetPermissions = () => {
return useQuery({
queryKey: ["roles", "permissions"],
queryFn: api.getPermissions,
});
};
export const useGetRoleById = (roleId: string | undefined) => {
return useQuery({
queryKey: ["role", roleId],
queryFn: () => api.getRoleById(roleId!),
enabled: !!roleId,
});
};
export const useCreateRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createRole,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["roles"] });
},
});
};
export const useUpdateRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
roleId,
params,
}: {
roleId: string;
params: CreateRoleType;
}) => api.updateRole(roleId, params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["roles"] });
},
});
};
export const useDeleteRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deleteRole,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["roles"] });
},
});
};
+41
View File
@@ -0,0 +1,41 @@
import axios from "@/config/axios";
import type {
RolesResponseType,
PermissionsResponseType,
RoleDetailResponseType,
CreateRoleType,
} from "../types/Types";
export const getRoles = async () => {
const { data } = await axios.get<RolesResponseType>("/admin/roles");
return data;
};
export const getPermissions = async () => {
const { data } = await axios.get<PermissionsResponseType>(
"/admin/roles/permissions"
);
return data;
};
export const getRoleById = async (roleId: string) => {
const { data } = await axios.get<RoleDetailResponseType>(
`/admin/roles/${roleId}`
);
return data;
};
export const createRole = async (params: CreateRoleType) => {
const { data } = await axios.post("/admin/role", params);
return data;
};
export const updateRole = async (roleId: string, params: CreateRoleType) => {
const { data } = await axios.patch(`/admin/roles/${roleId}`, params);
return data;
};
export const deleteRole = async (roleId: string) => {
const { data } = await axios.delete(`/admin/roles/${roleId}`);
return data;
};
+30
View File
@@ -0,0 +1,30 @@
import type { RowDataType } from "@/components/types/TableTypes";
import type { BaseResponse } from "@/shared/types/Types";
export interface PermissionItemType {
id: string;
createdAt: string;
deletedAt: string | null;
name: string;
title: string;
}
export interface RoleItemType extends RowDataType {
id: string;
createdAt: string;
deletedAt: string | null;
name: string;
title: string;
isSystem: boolean;
permissions: PermissionItemType[];
}
export type RolesResponseType = BaseResponse<RoleItemType[]>;
export type PermissionsResponseType = BaseResponse<PermissionItemType[]>;
export type RoleDetailResponseType = BaseResponse<RoleItemType>;
export type CreateRoleType = {
name: string;
title: string;
permissionIds: string[];
};
+7
View File
@@ -48,6 +48,9 @@ import UpdateUser from "@/pages/user/Update";
import AdminList from "@/pages/admin/List";
import CreateAdmin from "@/pages/admin/Create";
import EditAdmin from "@/pages/admin/Update";
import RoleList from "@/pages/role/List";
import CreateRole from "@/pages/role/Create";
import UpdateRole from "@/pages/role/Update";
import RequestDetail from "@/pages/requests/Detail";
import ConvertToOrders from "@/pages/order/ConvertToOrders";
import OrderPrint from "@/pages/order/Print";
@@ -112,6 +115,10 @@ const MainRouter: FC = () => {
<Route path={Paths.admin.create} element={<CreateAdmin />} />
<Route path={Paths.admin.update + ":id"} element={<EditAdmin />} />
<Route path={Paths.role.list} element={<RoleList />} />
<Route path={Paths.role.create} element={<CreateRole />} />
<Route path={Paths.role.update + ":id"} element={<UpdateRole />} />
<Route path={Paths.features.list} element={<FeaturesList />} />
<Route path={Paths.features.create} element={<CreateFeature />} />
<Route path={Paths.service.print} element={<PrintService />} />
+8
View File
@@ -13,6 +13,7 @@ import {
People,
Printer,
Receipt21,
ShieldSecurity,
Teacher,
User,
} from 'iconsax-react';
@@ -115,6 +116,13 @@ const SideBar: FC = () => {
isActive={isActive('admin')}
link={Paths.admin.list}
/>
<SideBarItem
icon={<ShieldSecurity size={iconSizeSideBar} color="#4F5260" />}
title={'نقش‌ها'}
isActive={isActive('role')}
link={Paths.role.list}
/>
</div>
<div className="mt-10 px-6 text-description text-sm font-bold">{t('sidebar.other')}</div>