list and create admin

This commit is contained in:
hamid zarghami
2025-09-28 14:57:33 +03:30
parent 992f839a91
commit 8e3c4401b5
7 changed files with 612 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
import { useFormik } from 'formik'
import { type FC } from 'react'
import { useGetPermissions, useCreateAdmin } from './hooks/useAdminData'
import type { CreateAdminType } from './types/Types'
import * as Yup from 'yup'
import { useNavigate } from 'react-router-dom'
import Input from '../../components/Input'
import MultiSelectSearchable from '../../components/MultiSelectSearchable'
import Button from '../../components/Button'
import { TickCircle } from 'iconsax-react'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/helpers/utils'
import { Pages } from '../../config/Pages'
const Create: FC = () => {
const navigate = useNavigate()
const { data: permissionsData } = useGetPermissions()
const createAdminMutation = useCreateAdmin()
const formik = useFormik<CreateAdminType>({
initialValues: {
fullName: "",
userName: "",
email: "",
phoneNumber: "",
permissions: [],
password: "",
},
validationSchema: Yup.object({
fullName: Yup.string().required("نام کامل الزامی است"),
userName: Yup.string().required("نام کاربری الزامی است"),
email: Yup.string().email("ایمیل معتبر نیست").required("ایمیل الزامی است"),
phoneNumber: Yup.string().required("شماره تلفن الزامی است"),
permissions: Yup.array().min(1, "دسترسی های الزامی است").required("دسترسی های الزامی است"),
password: Yup.string().min(6, "رمز عبور باید حداقل ۶ کاراکتر باشد").required("رمز عبور الزامی است"),
}),
onSubmit: async (values) => {
createAdminMutation.mutate(values, {
onSuccess: () => {
toast.success('ادمین با موفقیت ایجاد شد')
navigate(Pages.admin.list)
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
}
})
const permissionsOptions = permissionsData?.results?.permissions?.map((permission) => ({
value: permission._id,
label: permission.name
})) || []
return (
<div className='mt-4'>
<div className='flex justify-between items-center'>
<div className='flex items-center gap-3'>
<h1>ساخت ادمین جدید</h1>
</div>
<div>
<Button
className='px-6'
onClick={() => formik.handleSubmit()}
isLoading={createAdminMutation.isPending}
disabled={!formik.isValid || createAdminMutation.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={16} color='#fff' />
<div>ثبت ادمین</div>
</div>
</Button>
</div>
</div>
<div className='flex gap-6 xl:mt-8 mt-6'>
{/* اطلاعات اصلی */}
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
<h2 className='text-lg font-medium mb-6'>اطلاعات اصلی</h2>
<div className='space-y-6'>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
<Input
label='نام کامل'
placeholder='مثال: علی احمدی'
{...formik.getFieldProps('fullName')}
error_text={formik.touched.fullName && formik.errors.fullName ? formik.errors.fullName : ''}
/>
<Input
label='نام کاربری'
placeholder='مثال: ali_ahmadi'
{...formik.getFieldProps('userName')}
error_text={formik.touched.userName && formik.errors.userName ? formik.errors.userName : ''}
/>
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
<Input
label='ایمیل'
placeholder='مثال: ali@example.com'
type='email'
{...formik.getFieldProps('email')}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : ''}
/>
<Input
label='شماره تلفن'
placeholder='مثال: ۰۹۱۲۳۴۵۶۷۸۹'
{...formik.getFieldProps('phoneNumber')}
error_text={formik.touched.phoneNumber && formik.errors.phoneNumber ? formik.errors.phoneNumber : ''}
/>
</div>
<Input
label='رمز عبور'
placeholder='رمز عبور را وارد کنید'
type='password'
{...formik.getFieldProps('password')}
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
/>
</div>
</div>
{/* دسترسی‌ها */}
<div className='w-80 space-y-6'>
<div className='text-sm bg-white py-8 px-6 rounded-3xl'>
<h2 className='text-lg font-medium mb-6'>دسترسیها</h2>
<div className='space-y-6'>
<MultiSelectSearchable
label='دسترسی‌های ادمین'
placeholder='انتخاب دسترسی‌ها'
options={permissionsOptions}
value={formik.values.permissions}
onChange={(value) => formik.setFieldValue('permissions', value)}
error_text={formik.touched.permissions && formik.errors.permissions ? String(formik.errors.permissions) : ''}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default Create
+136
View File
@@ -0,0 +1,136 @@
import { type FC, useState } from 'react'
import { useDeleteAdmin, useGetAdmins } from './hooks/useAdminData'
import { useQueryClient } from '@tanstack/react-query'
import { type Admin } from './types/Types'
import PageLoading from '../../components/PageLoading'
import Error from '../../components/Error'
import PaginationUi from '../../components/PaginationUi'
import Td from '../../components/Td'
import { Calendar, Call, Card, User as UserIcon } from 'iconsax-react'
import Button from '@/components/Button'
import { useNavigate } from 'react-router-dom'
import { Pages } from '@/config/Pages'
import TrashWithConfrim from '../../components/TrashWithConfrim'
const List: FC = () => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [currentPage, setCurrentPage] = useState(1);
const { data: adminsData, isLoading, error } = useGetAdmins(currentPage);
const deleteAdminMutation = useDeleteAdmin();
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
);
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText="خطا در بارگذاری ادمین‌ها" />
</div>
);
}
const admins = adminsData?.results?.admins || [];
const pager = adminsData?.results?.pager;
const formatDate = (dateString: string) => {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('fa-IR');
};
return (
<div>
<div className='flex justify-end mt-5'>
<Button
label='افزودن ادمین'
onClick={() => navigate(`${Pages.admin.create}`)}
className='w-fit'
/>
</div>
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
<Td text={'نام و نام خانوادگی'} />
<Td text={'نام کاربری'} />
<Td text={'ایمیل'} />
<Td text={'شماره تلفن'} />
<Td text={'نقش'} />
<Td text={'تاریخ ایجاد'} />
<Td text={'عملیات'} />
</tr>
</thead>
<tbody>
{admins.length === 0 ? (
<tr className='tr'>
<td colSpan={7} className="text-center py-8 text-gray-500">
هیچ ادمینی یافت نشد
</td>
</tr>
) : (
admins.map((admin: Admin) => (
<tr key={admin._id} className='tr'>
<Td text="">
<div className="flex items-center gap-2">
<UserIcon size={16} color="#8C90A3" />
<span>{admin.fullName || '-'}</span>
</div>
</Td>
<Td text={admin.userName} />
<Td text="">
<div className="flex items-center gap-2">
<Card size={16} color="#8C90A3" />
<span>{admin.email}</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-2">
<Call size={16} color="#8C90A3" />
<span>{admin.phoneNumber}</span>
</div>
</Td>
<Td text={admin.role?.name || '-'} />
<Td text="">
<div className="flex items-center gap-2">
<Calendar size={16} color="#8C90A3" />
<span>{formatDate(admin.createdAt)}</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-2">
<TrashWithConfrim
onDelete={() => {
deleteAdminMutation.mutate(admin._id, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admins'] })
}
})
}}
/>
</div>
</Td>
</tr>
))
)}
</tbody>
</table>
</div>
<PaginationUi
pager={pager}
onPageChange={(page) => {
setCurrentPage(page);
}}
/>
</div>
)
}
export default List
+159
View File
@@ -0,0 +1,159 @@
import { useFormik } from 'formik'
import { type FC, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { useGetPermissions, useGetAdminDetail, useUpdateAdmin } from './hooks/useAdminData'
import type { UpdateAdminType } from './types/Types'
import * as Yup from 'yup'
import { useNavigate } from 'react-router-dom'
import Input from '../../components/Input'
import MultiSelectSearchable from '../../components/MultiSelectSearchable'
import Button from '../../components/Button'
import { TickCircle } from 'iconsax-react'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/helpers/utils'
import { Pages } from '../../config/Pages'
const Update: FC = () => {
const navigate = useNavigate()
const { id } = useParams<{ id: string }>()
const { data: permissionsData } = useGetPermissions()
const { data: adminDetail, isLoading: adminDetailLoading, error: adminDetailError } = useGetAdminDetail(id!)
const updateAdminMutation = useUpdateAdmin()
const formik = useFormik<UpdateAdminType>({
initialValues: {
fullName: "",
userName: "",
email: "",
phoneNumber: "",
permissions: [],
},
validationSchema: Yup.object({
fullName: Yup.string().required("نام کامل الزامی است"),
userName: Yup.string().required("نام کاربری الزامی است"),
email: Yup.string().email("ایمیل معتبر نیست").required("ایمیل الزامی است"),
phoneNumber: Yup.string().required("شماره تلفن الزامی است"),
permissions: Yup.array().min(1, "دسترسی های الزامی است").required("دسترسی های الزامی است"),
}),
onSubmit: async (values) => {
updateAdminMutation.mutate({ id: id!, ...values }, {
onSuccess: () => {
toast.success('ادمین با موفقیت ویرایش شد')
navigate(Pages.admin.list)
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
}
})
useEffect(() => {
if (adminDetail) {
formik.setValues({
fullName: adminDetail.results.admin.fullName,
userName: adminDetail.results.admin.userName,
email: adminDetail.results.admin.email,
phoneNumber: adminDetail.results.admin.phoneNumber,
permissions: adminDetail.results.admin.permissions,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [adminDetail])
const permissionsOptions = permissionsData?.results?.permissions?.map((permission) => ({
value: permission._id,
label: permission.name
})) || []
if (adminDetailLoading) {
return <div>در حال بارگذاری...</div>
}
if (adminDetailError) {
return <div>خطا در بارگذاری دادهها</div>
}
return (
<div className='mt-4'>
<div className='flex justify-between items-center'>
<div className='flex items-center gap-3'>
<h1>ویرایش ادمین</h1>
</div>
<div>
<Button
className='px-6'
onClick={() => formik.handleSubmit()}
isLoading={updateAdminMutation.isPending}
disabled={!formik.isValid || updateAdminMutation.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={16} color='#fff' />
<div>ویرایش ادمین</div>
</div>
</Button>
</div>
</div>
<div className='flex gap-6 xl:mt-8 mt-6'>
{/* اطلاعات اصلی */}
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
<h2 className='text-lg font-medium mb-6'>اطلاعات اصلی</h2>
<div className='space-y-6'>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
<Input
label='نام کامل'
placeholder='مثال: علی احمدی'
{...formik.getFieldProps('fullName')}
error_text={formik.touched.fullName && formik.errors.fullName ? formik.errors.fullName : ''}
/>
<Input
label='نام کاربری'
placeholder='مثال: ali_ahmadi'
{...formik.getFieldProps('userName')}
error_text={formik.touched.userName && formik.errors.userName ? formik.errors.userName : ''}
/>
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
<Input
label='ایمیل'
placeholder='مثال: ali@example.com'
type='email'
{...formik.getFieldProps('email')}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : ''}
/>
<Input
label='شماره تلفن'
placeholder='مثال: ۰۹۱۲۳۴۵۶۷۸۹'
{...formik.getFieldProps('phoneNumber')}
error_text={formik.touched.phoneNumber && formik.errors.phoneNumber ? formik.errors.phoneNumber : ''}
/>
</div>
</div>
</div>
{/* دسترسی‌ها */}
<div className='w-80 space-y-6'>
<div className='text-sm bg-white py-8 px-6 rounded-3xl'>
<h2 className='text-lg font-medium mb-6'>دسترسیها</h2>
<div className='space-y-6'>
<MultiSelectSearchable
label='دسترسی‌های ادمین'
placeholder='انتخاب دسترسی‌ها'
options={permissionsOptions}
value={formik.values.permissions}
onChange={(value) => formik.setFieldValue('permissions', value)}
error_text={formik.touched.permissions && formik.errors.permissions ? String(formik.errors.permissions) : ''}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default Update
+42
View File
@@ -0,0 +1,42 @@
import * as api from "../service/AdminService";
import { useMutation, useQuery } from "@tanstack/react-query";
export const useGetAdmins = (page: number = 1, limit: number = 10) => {
return useQuery({
queryKey: ["admins", page, limit],
queryFn: () => api.getAdmins(page, limit),
});
};
export const useGetPermissions = () => {
return useQuery({
queryKey: ["permissions"],
queryFn: () => api.getPermissions(),
});
};
export const useCreateAdmin = () => {
return useMutation({
mutationFn: api.createAdmin,
});
};
export const useDeleteAdmin = () => {
return useMutation({
mutationFn: api.deleteAdmin,
});
};
export const useGetAdminDetail = (id: string) => {
return useQuery({
queryKey: ["admin", id],
queryFn: () => api.getAdminDetail(id),
enabled: !!id,
});
};
export const useUpdateAdmin = () => {
return useMutation({
mutationFn: api.updateAdmin,
});
};
+48
View File
@@ -0,0 +1,48 @@
import axios from "../../../config/axios";
import type {
GetPermissionsResponse,
CreateAdminType,
UpdateAdminType,
GetAdminsResponse,
Admin,
} from "../types/Types";
export const getAdmins = async (
page: number = 1,
limit: number = 10
): Promise<GetAdminsResponse> => {
const { data } = await axios.get(`/admin/admins`, {
params: { page, limit },
});
return data;
};
export const getPermissions = async (): Promise<GetPermissionsResponse> => {
const { data } = await axios.get(`/admin/permissions`);
return data;
};
export const createAdmin = async (admin: CreateAdminType) => {
const { data } = await axios.post(`/admin/create`, admin);
return data;
};
export const deleteAdmin = async (id: string) => {
const { data } = await axios.delete(`/admin/${id}/delete`);
return data;
};
export const getAdminDetail = async (
id: string
): Promise<{ status: number; success: boolean; results: { admin: Admin } }> => {
const { data } = await axios.get(`/admin/${id}`);
return data;
};
export const updateAdmin = async ({
id,
...admin
}: { id: string } & UpdateAdminType) => {
const { data } = await axios.put(`/admin/${id}/update`, admin);
return data;
};
+76
View File
@@ -0,0 +1,76 @@
export interface Permission {
_id: string;
name: string;
createdAt: string;
updatedAt: string;
}
export interface GetPermissionsResponse {
status: number;
success: boolean;
results: {
permissions: Permission[];
};
}
export type CreateAdminType = {
fullName: string;
userName: string;
email: string;
phoneNumber: string;
permissions: string[];
password: string;
};
export type UpdateAdminType = {
fullName: string;
userName: string;
email: string;
phoneNumber: string;
permissions: string[];
};
export interface AdminRole {
_id: string;
name: string;
createdAt: string;
updatedAt: string;
}
export interface CreatedBy {
_id: string;
fullName: string;
userName: string;
email: string;
}
export interface Admin {
_id: string;
fullName: string;
userName: string;
email: string;
phoneNumber: string;
role: AdminRole;
createdBy: CreatedBy;
createdAt: string;
updatedAt: string;
permissions: string[];
}
export interface Pager {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: boolean;
nextPage: string | null;
}
export interface GetAdminsResponse {
status: number;
success: boolean;
results: {
admins: Admin[];
pager: Pager;
};
}
+6
View File
@@ -33,6 +33,8 @@ import CouponList from '@/pages/Coupon/List'
import CreateCoupon from '@/pages/Coupon/Create'
import UpdateCoupon from '@/pages/Coupon/Update'
import UsersList from '@/pages/user/List'
import AdminList from '@/pages/admin/List'
import AdminCreate from '@/pages/admin/Create'
const MainRouter: FC = () => {
@@ -86,6 +88,10 @@ const MainRouter: FC = () => {
<Route path={`${Pages.coupon.detail}:id`} element={<UpdateCoupon />} />
<Route path={Pages.buyUsers.list} element={<UsersList />} />
<Route path={Pages.admin.list} element={<AdminList />} />
<Route path={Pages.admin.create} element={<AdminCreate />} />
</Routes>
</div>
</div>