Compare commits
4 Commits
74fec3d2be
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0369553c39 | |||
| 3676688849 | |||
| 7d4e2865bb | |||
| 8563508674 |
@@ -8,8 +8,8 @@ export const Paths = {
|
|||||||
create: '/product/create',
|
create: '/product/create',
|
||||||
update: '/product/update/',
|
update: '/product/update/',
|
||||||
category: {
|
category: {
|
||||||
create: '/product/category/create',
|
|
||||||
list: '/product/category/list',
|
list: '/product/category/list',
|
||||||
|
children: '/product/category/list/',
|
||||||
update: '/product/category/update/',
|
update: '/product/category/update/',
|
||||||
},
|
},
|
||||||
attribute: {
|
attribute: {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
|
||||||
|
/** Permission names aligned with API PermissionEnum. */
|
||||||
|
export const Permissions = {
|
||||||
|
DASHBOARD: 'dashboard',
|
||||||
|
VIEW_ORDERS: 'view_orders',
|
||||||
|
VIEW_ASSIGNED_ORDERS: 'view_assigned_orders',
|
||||||
|
VIEW_INVOICES: 'view_invoices',
|
||||||
|
VIEW_REQUESTS: 'view_requests',
|
||||||
|
MANAGE_PAYMENTS: 'manage_payments',
|
||||||
|
VIEW_USERS: 'view_users',
|
||||||
|
VIEW_PRINT_FORM: 'view_print_form',
|
||||||
|
MANAGE_ADMINS: 'manage_admins',
|
||||||
|
MANAGE_ROLES: 'manage_roles',
|
||||||
|
MANAGE_PRODUCTS: 'manage_products',
|
||||||
|
MANAGE_TICKETS: 'manage_tickets',
|
||||||
|
MANAGE_ANNOUNCEMENTS: 'manage_announcements',
|
||||||
|
MANAGE_CRITICISMS: 'manage_criticisms',
|
||||||
|
MANAGE_LEARNINGS: 'manage_learnings',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type PermissionName = (typeof Permissions)[keyof typeof Permissions]
|
||||||
|
|
||||||
|
export const ORDER_VIEW_PERMISSIONS: PermissionName[] = [
|
||||||
|
Permissions.VIEW_ORDERS,
|
||||||
|
Permissions.VIEW_ASSIGNED_ORDERS,
|
||||||
|
]
|
||||||
|
|
||||||
|
type LandingRoute = {
|
||||||
|
permissions: PermissionName | PermissionName[]
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sidebar-ordered landing candidates when choosing a post-login / fallback path. */
|
||||||
|
const LANDING_ROUTES: LandingRoute[] = [
|
||||||
|
{ permissions: Permissions.DASHBOARD, path: Paths.home },
|
||||||
|
{ permissions: ORDER_VIEW_PERMISSIONS, path: Paths.order.list },
|
||||||
|
{ permissions: Permissions.VIEW_INVOICES, path: Paths.perfomaInvoice.list },
|
||||||
|
{ permissions: Permissions.VIEW_REQUESTS, path: Paths.requests.list },
|
||||||
|
{ permissions: Permissions.MANAGE_PAYMENTS, path: Paths.payments.list },
|
||||||
|
{ permissions: Permissions.VIEW_USERS, path: Paths.users.list },
|
||||||
|
{ permissions: Permissions.VIEW_PRINT_FORM, path: Paths.print.list },
|
||||||
|
{ permissions: Permissions.MANAGE_ADMINS, path: Paths.admin.list },
|
||||||
|
{ permissions: Permissions.MANAGE_PRODUCTS, path: Paths.product.list },
|
||||||
|
{ permissions: Permissions.MANAGE_TICKETS, path: Paths.tickets.list },
|
||||||
|
{ permissions: Permissions.MANAGE_ANNOUNCEMENTS, path: Paths.announcement.list },
|
||||||
|
{ permissions: Permissions.MANAGE_CRITICISMS, path: Paths.criticisms.list },
|
||||||
|
{ permissions: Permissions.MANAGE_LEARNINGS, path: Paths.learning.list },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const hasPermission = (
|
||||||
|
userPermissions: string[],
|
||||||
|
required: string | string[],
|
||||||
|
): boolean => {
|
||||||
|
const requiredList = Array.isArray(required) ? required : [required]
|
||||||
|
return requiredList.some((permission) => userPermissions.includes(permission))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** First permitted app path, or profile if none match. */
|
||||||
|
export const getDefaultAdminPath = (permissions: string[]): string => {
|
||||||
|
for (const route of LANDING_ROUTES) {
|
||||||
|
if (hasPermission(permissions, route.permissions)) {
|
||||||
|
return route.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Paths.profile
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { getToken, removeRefreshToken, removeToken } from "./func";
|
import { getToken, removeRefreshToken, removeToken } from "./func";
|
||||||
|
import { useAuthStore } from "@/pages/auth/store/AuthStore";
|
||||||
|
|
||||||
type SessionAuthStore = {
|
type SessionAuthStore = {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
@@ -14,6 +15,7 @@ export const useSessionAuth = create<SessionAuthStore>((set) => ({
|
|||||||
removeToken();
|
removeToken();
|
||||||
removeRefreshToken();
|
removeRefreshToken();
|
||||||
window.isRefreshTokenExpired = false;
|
window.isRefreshTokenExpired = false;
|
||||||
|
useAuthStore.getState().reset();
|
||||||
set({ isAuthenticated: false });
|
set({ isAuthenticated: false });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
|
|||||||
import { extractErrorMessage, setRefreshToken } from '../../../config/func'
|
import { extractErrorMessage, setRefreshToken } from '../../../config/func'
|
||||||
import { setToken } from '../../../config/func'
|
import { setToken } from '../../../config/func'
|
||||||
import { toast } from '@/shared/toast'
|
import { toast } from '@/shared/toast'
|
||||||
import { Paths } from '@/config/Paths'
|
|
||||||
import { useSessionAuth } from '@/config/sessionAuth'
|
import { useSessionAuth } from '@/config/sessionAuth'
|
||||||
import { appNavigate } from '@/config/navigation'
|
import { appNavigate } from '@/config/navigation'
|
||||||
|
import { getDefaultAdminPath } from '@/config/permissions'
|
||||||
|
import type { BaseResponse } from '@/shared/types/Types'
|
||||||
|
import type { OtpVerifyResponseType } from '../types/AuthTypes'
|
||||||
|
|
||||||
const LoginStep2: FC = () => {
|
const LoginStep2: FC = () => {
|
||||||
|
|
||||||
@@ -39,11 +41,12 @@ const LoginStep2: FC = () => {
|
|||||||
otp: values.otp
|
otp: values.otp
|
||||||
}
|
}
|
||||||
otpVerify.mutate(params, {
|
otpVerify.mutate(params, {
|
||||||
onSuccess(data) {
|
onSuccess(data: BaseResponse<OtpVerifyResponseType>) {
|
||||||
setToken(data?.data?.tokens?.accessToken?.token)
|
setToken(data?.data?.tokens?.accessToken?.token)
|
||||||
setRefreshToken(data?.data?.tokens?.refreshToken?.token)
|
setRefreshToken(data?.data?.tokens?.refreshToken?.token)
|
||||||
useSessionAuth.getState().setAuthenticated(true)
|
useSessionAuth.getState().setAuthenticated(true)
|
||||||
appNavigate(Paths.home, { replace: true })
|
const permissions = data?.data?.admin?.permissions ?? []
|
||||||
|
appNavigate(getDefaultAdminPath(permissions), { replace: true })
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast(extractErrorMessage(error), 'error')
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { type AuthStoreType } from "../../auth/types/AuthTypes";
|
import { type AuthStoreType } from "../../auth/types/AuthTypes";
|
||||||
|
|
||||||
export const useAuthStore = create<AuthStoreType>((set) => ({
|
const initialState = {
|
||||||
phone: "",
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
stepLogin: 1,
|
||||||
|
devOtpCode: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthStoreType>((set) => ({
|
||||||
|
...initialState,
|
||||||
setPhone(value) {
|
setPhone(value) {
|
||||||
set({ phone: value });
|
set({ phone: value });
|
||||||
},
|
},
|
||||||
email: "",
|
|
||||||
setEmail(value) {
|
setEmail(value) {
|
||||||
set({ email: value });
|
set({ email: value });
|
||||||
},
|
},
|
||||||
stepLogin: 1,
|
|
||||||
setStepLogin(value) {
|
setStepLogin(value) {
|
||||||
set({ stepLogin: value });
|
set({ stepLogin: value });
|
||||||
},
|
},
|
||||||
devOtpCode: "",
|
|
||||||
setDevOtpCode(value) {
|
setDevOtpCode(value) {
|
||||||
set({ devOtpCode: value });
|
set({ devOtpCode: value });
|
||||||
},
|
},
|
||||||
|
reset() {
|
||||||
|
set({ ...initialState });
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type AuthStoreType = {
|
|||||||
/** TODO: remove before production — dev-only OTP from API response */
|
/** TODO: remove before production — dev-only OTP from API response */
|
||||||
devOtpCode: string;
|
devOtpCode: string;
|
||||||
setDevOtpCode: (value: string) => void;
|
setDevOtpCode: (value: string) => void;
|
||||||
|
reset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginWithPasswordType = {
|
export type LoginWithPasswordType = {
|
||||||
@@ -32,6 +33,22 @@ export type OtpVerifyType = {
|
|||||||
otp: string;
|
otp: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminLoginResponseType = {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
phone: string;
|
||||||
|
role: string;
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OtpVerifyResponseType = {
|
||||||
|
tokens: {
|
||||||
|
accessToken: { token: string };
|
||||||
|
refreshToken: { token: string };
|
||||||
|
};
|
||||||
|
admin: AdminLoginResponseType;
|
||||||
|
};
|
||||||
|
|
||||||
export type CheckHasAccountType = {
|
export type CheckHasAccountType = {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { toast } from 'react-toastify';
|
|||||||
import type { ErrorType } from '@/helpers/types';
|
import type { ErrorType } from '@/helpers/types';
|
||||||
import { extractErrorMessage } from '@/config/func';
|
import { extractErrorMessage } from '@/config/func';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
import BackButton from '@/components/BackButton'
|
||||||
|
|
||||||
const CreateProduct: FC = () => {
|
const CreateProduct: FC = () => {
|
||||||
|
|
||||||
@@ -71,8 +73,10 @@ const CreateProduct: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
<h1 className='text-lg font-light'>محصول جدید</h1>
|
<BackButton to={Paths.product.list} />
|
||||||
|
<h1 className='text-lg font-light'>محصول جدید</h1>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className='w-fit px-6'
|
className='w-fit px-6'
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
|
|||||||
+29
-23
@@ -1,7 +1,6 @@
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import Filters, { type FilterValues } from '@/components/Filters'
|
import Filters, { type FilterValues } from '@/components/Filters'
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import Tabs from '@/components/Tabs'
|
|
||||||
import { AddSquare, Edit, More2 } from 'iconsax-react'
|
import { AddSquare, Edit, More2 } from 'iconsax-react'
|
||||||
import { type FC, useMemo, useState } from 'react'
|
import { type FC, useMemo, useState } from 'react'
|
||||||
import { useDeleteProduct, useDuplicateProduct, useGetCategory, useGetProducts } from './hooks/useProductData'
|
import { useDeleteProduct, useDuplicateProduct, useGetCategory, useGetProducts } from './hooks/useProductData'
|
||||||
@@ -11,39 +10,44 @@ import TrashWithConfrim from '@/components/TrashWithConfrim'
|
|||||||
import CopyWithConfirm from '@/components/CopyWithConfirm'
|
import CopyWithConfirm from '@/components/CopyWithConfirm'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import type { CategoryType } from './types/Types'
|
||||||
|
|
||||||
|
const flattenCategoryOptions = (categories: CategoryType[]) => {
|
||||||
|
const options: { label: string; value: string }[] = []
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
options.push({ label: category.title, value: category.id })
|
||||||
|
for (const child of category.children ?? []) {
|
||||||
|
options.push({ label: `— ${child.title}`, value: child.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
const ProductList: FC = () => {
|
const ProductList: FC = () => {
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const [categoryId, setCategoryId] = useState<string>('')
|
const [categoryId, setCategoryId] = useState<string>('')
|
||||||
|
|
||||||
const { data: categoriesData } = useGetCategory({ limit: 1000 })
|
const { data: categoriesData } = useGetCategory({ limit: 1000, parentId: 'null' })
|
||||||
const { data: products, refetch } = useGetProducts({
|
const { data: products, refetch } = useGetProducts({
|
||||||
page,
|
page,
|
||||||
search: search || undefined,
|
search: search || undefined,
|
||||||
categoryId: categoryId || undefined,
|
categoryId: categoryId || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const categoryTabs = useMemo(
|
const categoryOptions = useMemo(
|
||||||
() => [
|
() => flattenCategoryOptions((categoriesData?.data ?? []) as CategoryType[]),
|
||||||
{ label: 'همه', value: '' },
|
|
||||||
...(categoriesData?.data?.map((category) => ({
|
|
||||||
label: category.title,
|
|
||||||
value: category.id,
|
|
||||||
})) ?? []),
|
|
||||||
],
|
|
||||||
[categoriesData?.data],
|
[categoriesData?.data],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFiltersChange = (filters: FilterValues) => {
|
const handleFiltersChange = (filters: FilterValues) => {
|
||||||
setSearch((filters.search as string) ?? '')
|
setSearch(filters.search ?? '')
|
||||||
|
setCategoryId(filters.categoryId ?? '')
|
||||||
setPage(1)
|
setPage(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCategoryTabChange = (tab: string) => {
|
|
||||||
setCategoryId(tab)
|
|
||||||
setPage(1)
|
|
||||||
}
|
|
||||||
const { mutate: deleteProduct, isPending: isDeleting } = useDeleteProduct()
|
const { mutate: deleteProduct, isPending: isDeleting } = useDeleteProduct()
|
||||||
const { mutate: duplicateProduct, isPending: isDuplicating } = useDuplicateProduct()
|
const { mutate: duplicateProduct, isPending: isDuplicating } = useDuplicateProduct()
|
||||||
const [duplicatingId, setDuplicatingId] = useState<string | null>(null)
|
const [duplicatingId, setDuplicatingId] = useState<string | null>(null)
|
||||||
@@ -98,14 +102,6 @@ const ProductList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8'>
|
|
||||||
<Tabs
|
|
||||||
items={categoryTabs}
|
|
||||||
activeTab={categoryId}
|
|
||||||
onTabChange={handleCategoryTabChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Filters
|
<Filters
|
||||||
fields={[
|
fields={[
|
||||||
@@ -113,6 +109,12 @@ const ProductList: FC = () => {
|
|||||||
name: 'search',
|
name: 'search',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: 'جستجوی محصول'
|
placeholder: 'جستجوی محصول'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categoryId',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: 'دستهبندی',
|
||||||
|
options: categoryOptions,
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onChange={handleFiltersChange}
|
onChange={handleFiltersChange}
|
||||||
@@ -152,6 +154,10 @@ const ProductList: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'order',
|
||||||
|
title: 'ترتیب',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
title: '',
|
title: '',
|
||||||
|
|||||||
@@ -3,39 +3,45 @@ import { useFormik } from 'formik'
|
|||||||
import { useState, type FC } from 'react'
|
import { useState, type FC } from 'react'
|
||||||
import type { CreateCategoryType } from '../types/Types'
|
import type { CreateCategoryType } from '../types/Types'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { useCreateCategory, useGetCategory } from '../hooks/useProductData'
|
import { useCreateCategory } from '../hooks/useProductData'
|
||||||
import Select from '@/components/Select'
|
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { AddSquare } from 'iconsax-react'
|
import { AddSquare, TickCircle } from 'iconsax-react'
|
||||||
import UploadBox from '@/components/UploadBox'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import SwitchComponent from '@/components/Switch'
|
import SwitchComponent from '@/components/Switch'
|
||||||
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
import { Paths } from '@/config/Paths'
|
|
||||||
import BackButton from '@/components/BackButton'
|
|
||||||
|
|
||||||
const ProductCategory: FC = () => {
|
type Props = {
|
||||||
|
parentId?: string
|
||||||
|
refetch: () => void
|
||||||
|
}
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const CreateCategory: FC<Props> = ({ parentId, refetch }) => {
|
||||||
const { data } = useGetCategory()
|
|
||||||
const { mutate, isPending } = useCreateCategory()
|
const { mutate, isPending } = useCreateCategory()
|
||||||
const { mutate: upload, isPending: isUploading } = useSingleUpload()
|
const { mutate: upload, isPending: isUploading } = useSingleUpload()
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = useState(false)
|
||||||
const [file, setFile] = useState<File>()
|
const [file, setFile] = useState<File>()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setShowModal(false)
|
||||||
|
setFile(undefined)
|
||||||
|
formik.resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
const handleSave = (values: CreateCategoryType) => {
|
const handleSave = (values: CreateCategoryType) => {
|
||||||
const payload: CreateCategoryType = {
|
const payload: CreateCategoryType = {
|
||||||
...values,
|
...values,
|
||||||
parentId: values.parentId || undefined,
|
parentId: parentId || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
mutate(payload, {
|
mutate(payload, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('با موفقیت ذخیره شد')
|
toast.success('با موفقیت ذخیره شد')
|
||||||
navigate(Paths.product.category.list)
|
refetch()
|
||||||
|
handleClose()
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
@@ -69,55 +75,39 @@ const ProductCategory: FC = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div>
|
||||||
<div className='flex justify-between items-center'>
|
<Button
|
||||||
<div className='flex items-center gap-4'>
|
className='w-fit px-6'
|
||||||
<BackButton to={Paths.product.category.list} />
|
onClick={() => setShowModal(true)}
|
||||||
<h1 className='text-lg font-light'>دسته جدید</h1>
|
>
|
||||||
|
<div className='flex gap-1.5'>
|
||||||
|
<AddSquare size={18} color='black' />
|
||||||
|
<div className='text-[13px] font-light'>دستهبندی جدید</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
</Button>
|
||||||
className='w-fit px-6'
|
|
||||||
isLoading={isPending || isUploading}
|
|
||||||
onClick={() => formik.handleSubmit()}
|
|
||||||
>
|
|
||||||
<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 '>
|
|
||||||
|
|
||||||
<div>
|
<DefaulModal
|
||||||
|
open={showModal}
|
||||||
|
close={handleClose}
|
||||||
|
isHeader
|
||||||
|
title_header='دسته جدید'
|
||||||
|
>
|
||||||
|
<div className='mt-5'>
|
||||||
<UploadBox
|
<UploadBox
|
||||||
label='تصویر دسته (اختیاری)'
|
label='تصویر دسته (اختیاری)'
|
||||||
onChange={(files) => setFile(files[0])}
|
onChange={(files) => setFile(files[0])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='rowTwoInput mt-6'>
|
<div className='mt-5'>
|
||||||
<Input
|
<Input
|
||||||
label='عنوان'
|
label='عنوان'
|
||||||
{...formik.getFieldProps('title')}
|
{...formik.getFieldProps('title')}
|
||||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
|
||||||
label='انتخاب والد درسته'
|
|
||||||
placeholder='اختیاری'
|
|
||||||
items={data?.data?.map((item) => {
|
|
||||||
return {
|
|
||||||
label: item.title,
|
|
||||||
value: item.id + ''
|
|
||||||
}
|
|
||||||
}) || []}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
name='parentId'
|
|
||||||
value={formik.values.parentId || ''}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-5'>
|
||||||
<Input
|
<Input
|
||||||
label='ترتیب اولویت'
|
label='ترتیب اولویت'
|
||||||
{...formik.getFieldProps('order')}
|
{...formik.getFieldProps('order')}
|
||||||
@@ -125,15 +115,31 @@ const ProductCategory: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-5'>
|
||||||
<SwitchComponent
|
<SwitchComponent
|
||||||
active={formik.values.isActive}
|
active={formik.values.isActive}
|
||||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className='mt-6 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
className='w-fit px-6'
|
||||||
|
isLoading={isPending || isUploading}
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<TickCircle
|
||||||
|
size={18}
|
||||||
|
color='black'
|
||||||
|
/>
|
||||||
|
<div>ذخیره</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProductCategory
|
export default CreateCategory
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
import { type FC, useMemo } from 'react'
|
import { type FC, useMemo } from 'react'
|
||||||
import PresignedImage from '@/components/PresignedImage'
|
import PresignedImage from '@/components/PresignedImage'
|
||||||
import { useDeleteCategory, useGetCategory } from '../hooks/useProductData'
|
import { useDeleteCategory, useGetCategory, useGetCategoryDetail } from '../hooks/useProductData'
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
|
import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
|
||||||
import type { CategoryType } from '../types/Types'
|
import type { CategoryType } from '../types/Types'
|
||||||
import Button from '@/components/Button'
|
import { ArrowLeft2, Edit } from 'iconsax-react'
|
||||||
import { AddSquare, Edit } from 'iconsax-react'
|
|
||||||
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
import BackButton from '@/components/BackButton'
|
import BackButton from '@/components/BackButton'
|
||||||
|
import CreateCategory from './Create'
|
||||||
|
|
||||||
const CategoryList: FC = () => {
|
const CategoryList: FC = () => {
|
||||||
|
const { parentId } = useParams<{ parentId?: string }>()
|
||||||
|
|
||||||
const { data, refetch } = useGetCategory()
|
const { data, refetch } = useGetCategory({
|
||||||
|
limit: 1000,
|
||||||
|
parentId: parentId ?? 'null',
|
||||||
|
})
|
||||||
|
const { data: parentDetail } = useGetCategoryDetail(parentId ?? '')
|
||||||
const { mutate, isPending } = useDeleteCategory()
|
const { mutate, isPending } = useDeleteCategory()
|
||||||
const categories = (data?.data ?? []) as (CategoryType & RowDataType)[]
|
const categories = (data?.data ?? []) as (CategoryType & RowDataType)[]
|
||||||
|
|
||||||
|
const parentCategory = parentDetail?.data
|
||||||
|
const backTo = parentCategory?.parent?.id
|
||||||
|
? Paths.product.category.children + parentCategory.parent.id
|
||||||
|
: Paths.product.category.list
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
mutate(id, {
|
mutate(id, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -39,19 +49,34 @@ const CategoryList: FC = () => {
|
|||||||
{
|
{
|
||||||
title: 'عنوان',
|
title: 'عنوان',
|
||||||
key: 'title',
|
key: 'title',
|
||||||
|
render: (item) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={Paths.product.category.children + item.id}
|
||||||
|
className='inline-flex items-center gap-1.5 text-sm text-black hover:opacity-70'
|
||||||
|
>
|
||||||
|
<span>{item.title}</span>
|
||||||
|
<ArrowLeft2 size={16} color='#8C90A3' />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'والد',
|
title: 'زیر دستهها',
|
||||||
key: 'parent',
|
|
||||||
render: (item) => item.parent?.title || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'تعداد زیر دسته',
|
|
||||||
key: 'children',
|
key: 'children',
|
||||||
render: (item) => <div>{item.children?.length || 0}</div>
|
render: (item) => {
|
||||||
|
if (!item.children?.length) return '0'
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-0.5'>
|
||||||
|
{item.children.map((child) => (
|
||||||
|
<span key={child.id}>{child.title}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'اولویت',
|
title: 'ترتیب',
|
||||||
key: 'order',
|
key: 'order',
|
||||||
render: (item) => item.order ?? '-',
|
render: (item) => item.order ?? '-',
|
||||||
},
|
},
|
||||||
@@ -75,29 +100,26 @@ const CategoryList: FC = () => {
|
|||||||
}
|
}
|
||||||
], [isPending])
|
], [isPending])
|
||||||
|
|
||||||
|
const title = parentCategory
|
||||||
|
? `زیر دستههای «${parentCategory.title}»`
|
||||||
|
: 'دسته بندی محصولات'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<BackButton to={Paths.product.list} />
|
<BackButton to={parentId ? backTo : Paths.product.list} />
|
||||||
<h1 className='text-lg font-light'>دسته بندی محصولات</h1>
|
<h1 className='text-lg font-light'>{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={Paths.product.category.create}>
|
<CreateCategory parentId={parentId} refetch={refetch} />
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Table<CategoryType & RowDataType>
|
<Table<CategoryType & RowDataType>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={categories}
|
data={categories}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type FC, type SelectHTMLAttributes } from 'react'
|
import { type FC, type SelectHTMLAttributes, useMemo } from 'react'
|
||||||
import { useGetCategory } from '../hooks/useProductData'
|
import { useGetCategory } from '../hooks/useProductData'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
|
import type { CategoryType } from '../types/Types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error_text?: string,
|
error_text?: string,
|
||||||
@@ -9,6 +10,19 @@ type Props = {
|
|||||||
clearable?: boolean,
|
clearable?: boolean,
|
||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
|
const flattenCategoryOptions = (categories: CategoryType[]) => {
|
||||||
|
const options: { label: string; value: string }[] = []
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
options.push({ label: category.title, value: category.id })
|
||||||
|
for (const child of category.children ?? []) {
|
||||||
|
options.push({ label: `— ${child.title}`, value: child.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
const CategoriesSelect: FC<Props> = ({
|
const CategoriesSelect: FC<Props> = ({
|
||||||
isDisableShowLable,
|
isDisableShowLable,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -16,8 +30,12 @@ const CategoriesSelect: FC<Props> = ({
|
|||||||
clearable = true,
|
clearable = true,
|
||||||
...selectProps
|
...selectProps
|
||||||
}) => {
|
}) => {
|
||||||
|
const { data: categories } = useGetCategory({ limit: 1000, parentId: 'null' })
|
||||||
|
|
||||||
const { data: categories } = useGetCategory()
|
const items = useMemo(
|
||||||
|
() => flattenCategoryOptions((categories?.data ?? []) as CategoryType[]),
|
||||||
|
[categories?.data],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -25,12 +43,7 @@ const CategoriesSelect: FC<Props> = ({
|
|||||||
placeholder={placeholder || 'انتخاب'}
|
placeholder={placeholder || 'انتخاب'}
|
||||||
error_text={error_text}
|
error_text={error_text}
|
||||||
clearable={clearable}
|
clearable={clearable}
|
||||||
items={categories?.data?.map((item) => {
|
items={items}
|
||||||
return {
|
|
||||||
label: item.title,
|
|
||||||
value: item.id + ''
|
|
||||||
}
|
|
||||||
}) || []}
|
|
||||||
{...selectProps}
|
{...selectProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ export const useGetCategory = (params?: api.GetCategoryParams) => {
|
|||||||
const page = params?.page ?? 1;
|
const page = params?.page ?? 1;
|
||||||
const limit = params?.limit;
|
const limit = params?.limit;
|
||||||
const search = params?.search;
|
const search = params?.search;
|
||||||
|
const parentId = params?.parentId;
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["category", page, limit, search],
|
queryKey: ["category", page, limit, search, parentId],
|
||||||
queryFn: () => api.getCategory({ page, limit, search }),
|
queryFn: () => api.getCategory({ page, limit, search, parentId }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type GetCategoryParams = {
|
|||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
parentId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategory = async (params?: GetCategoryParams) => {
|
export const getCategory = async (params?: GetCategoryParams) => {
|
||||||
@@ -13,6 +14,7 @@ export const getCategory = async (params?: GetCategoryParams) => {
|
|||||||
page: params?.page,
|
page: params?.page,
|
||||||
limit: params?.limit,
|
limit: params?.limit,
|
||||||
search: params?.search || undefined,
|
search: params?.search || undefined,
|
||||||
|
parentId: params?.parentId || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
+70
-55
@@ -1,8 +1,13 @@
|
|||||||
import { type FC } from "react";
|
import { type FC, type ReactNode } from "react";
|
||||||
import { Route, Routes, useLocation } from "react-router-dom";
|
import { Route, Routes, useLocation } from "react-router-dom";
|
||||||
import Home from "../pages/home/Home";
|
import Home from "../pages/home/Home";
|
||||||
import AppLayout from "../shared/AppLayout";
|
import AppLayout from "../shared/AppLayout";
|
||||||
|
import PermissionGuard from "../shared/PermissionGuard";
|
||||||
import { Paths } from "@/config/Paths";
|
import { Paths } from "@/config/Paths";
|
||||||
|
import {
|
||||||
|
ORDER_VIEW_PERMISSIONS,
|
||||||
|
Permissions,
|
||||||
|
} from "@/config/permissions";
|
||||||
import ProformaInvoice from "@/pages/invoice/ProformaInvoice";
|
import ProformaInvoice from "@/pages/invoice/ProformaInvoice";
|
||||||
import CreateInvoice from "@/pages/invoice/Create";
|
import CreateInvoice from "@/pages/invoice/Create";
|
||||||
import RequestList from "@/pages/requests/RequestList";
|
import RequestList from "@/pages/requests/RequestList";
|
||||||
@@ -23,7 +28,6 @@ import LearningList from "@/pages/learning/List";
|
|||||||
import LearningCreate from "@/pages/learning/Create";
|
import LearningCreate from "@/pages/learning/Create";
|
||||||
import LearningCategory from "@/pages/learning/Category";
|
import LearningCategory from "@/pages/learning/Category";
|
||||||
import CategoryList from "@/pages/product/category/List";
|
import CategoryList from "@/pages/product/category/List";
|
||||||
import CreateCategory from "@/pages/product/category/Create";
|
|
||||||
import UpdateCategory from "@/pages/product/category/Update";
|
import UpdateCategory from "@/pages/product/category/Update";
|
||||||
import UpdateProduct from "@/pages/product/Update";
|
import UpdateProduct from "@/pages/product/Update";
|
||||||
import CreateAttribute from "@/pages/product/attribute/Create";
|
import CreateAttribute from "@/pages/product/attribute/Create";
|
||||||
@@ -59,6 +63,13 @@ import TicketList from "@/pages/ticket/TicketList";
|
|||||||
import TicketDetail from "@/pages/ticket/Detail";
|
import TicketDetail from "@/pages/ticket/Detail";
|
||||||
import PaymentsList from "@/pages/payment/List";
|
import PaymentsList from "@/pages/payment/List";
|
||||||
|
|
||||||
|
const withPermission = (
|
||||||
|
permission: string | string[],
|
||||||
|
element: ReactNode,
|
||||||
|
) => (
|
||||||
|
<PermissionGuard permission={permission}>{element}</PermissionGuard>
|
||||||
|
);
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const isPrintPreview = /\/order\/print\/[^/]+\/preview/.test(location.pathname)
|
const isPrintPreview = /\/order\/print\/[^/]+\/preview/.test(location.pathname)
|
||||||
@@ -74,74 +85,78 @@ const MainRouter: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={withPermission(Permissions.DASHBOARD, <Home />)} />
|
||||||
<Route path={Paths.home} element={<Home />} />
|
<Route path={Paths.home} element={withPermission(Permissions.DASHBOARD, <Home />)} />
|
||||||
<Route path={Paths.profile} element={<Profile />} />
|
<Route path={Paths.profile} element={<Profile />} />
|
||||||
<Route path={Paths.perfomaInvoice.list} element={<ProformaInvoice />} />
|
|
||||||
<Route path={Paths.payments.list} element={<PaymentsList />} />
|
|
||||||
<Route path={Paths.perfomaInvoice.create} element={<CreateInvoice />} />
|
|
||||||
<Route path={Paths.perfomaInvoice.detail + ":id"} element={<DetailPerfomaInvoice />} />
|
|
||||||
<Route path={Paths.perfomaInvoice.update + ":id"} element={<UpdateInvoice />} />
|
|
||||||
<Route path={Paths.requests.list} element={<RequestList />} />
|
|
||||||
<Route path={Paths.requests.detail + ":id"} element={<RequestDetail />} />
|
|
||||||
<Route path={Paths.product.list} element={<ProductList />} />
|
|
||||||
<Route path={Paths.product.create} element={<CreateProduct />} />
|
|
||||||
<Route path={Paths.product.update + ":id"} element={<UpdateProduct />} />
|
|
||||||
<Route path={Paths.product.attribute.create + ":id"} element={<CreateAttribute />} />
|
|
||||||
<Route path={Paths.product.attribute.list + ":id"} element={<AttributeList />} />
|
|
||||||
<Route path={Paths.product.attribute.update + ":id"} element={<UpdateAttribute />} />
|
|
||||||
<Route path={Paths.product.attributeValue.list + ":id"} element={<AttributeValues />} />
|
|
||||||
<Route path={Paths.product.category.list} element={<CategoryList />} />
|
|
||||||
<Route path={Paths.product.category.create} element={<CreateCategory />} />
|
|
||||||
<Route path={Paths.product.category.update + ":id"} element={<UpdateCategory />} />
|
|
||||||
|
|
||||||
<Route path={Paths.formBuilder.list + ":id/:type"} element={<FormBuilderList />} />
|
<Route path={Paths.perfomaInvoice.list} element={withPermission(Permissions.VIEW_INVOICES, <ProformaInvoice />)} />
|
||||||
<Route path={Paths.formBuilder.create + ":id/:type"} element={<FormBuilderCreate />} />
|
<Route path={Paths.perfomaInvoice.create} element={withPermission(Permissions.VIEW_INVOICES, <CreateInvoice />)} />
|
||||||
<Route path={Paths.formBuilder.update + ":id"} element={<FormBuilderUpdate />} />
|
<Route path={Paths.perfomaInvoice.detail + ":id"} element={withPermission(Permissions.VIEW_INVOICES, <DetailPerfomaInvoice />)} />
|
||||||
<Route path={Paths.formBuilder.values + ":id"} element={<FormBuilderValues />} />
|
<Route path={Paths.perfomaInvoice.update + ":id"} element={withPermission(Permissions.VIEW_INVOICES, <UpdateInvoice />)} />
|
||||||
|
|
||||||
<Route path={Paths.order.list} element={<OrdersList />} />
|
<Route path={Paths.payments.list} element={withPermission(Permissions.MANAGE_PAYMENTS, <PaymentsList />)} />
|
||||||
<Route path={Paths.order.details + ":id"} element={<OrderDetail />} />
|
|
||||||
<Route path={Paths.order.create} element={<NewOrder />} />
|
|
||||||
<Route path={Paths.order.edit + ":id"} element={<EditOrder />} />
|
|
||||||
<Route path={Paths.convertToOrder} element={<ConvertToOrders />} />
|
|
||||||
<Route path={Paths.orderPrint + ':id/preview'} element={<OrderPrintPreview />} />
|
|
||||||
<Route path={Paths.orderPrint + ':id'} element={<OrderPrint />} />
|
|
||||||
|
|
||||||
<Route path={Paths.tickets.list} element={<TicketList />} />
|
<Route path={Paths.requests.list} element={withPermission(Permissions.VIEW_REQUESTS, <RequestList />)} />
|
||||||
<Route path={Paths.tickets.detail + ":id"} element={<TicketDetail />} />
|
<Route path={Paths.requests.detail + ":id"} element={withPermission(Permissions.VIEW_REQUESTS, <RequestDetail />)} />
|
||||||
|
|
||||||
<Route path={Paths.print.list} element={<SectionList />} />
|
<Route path={Paths.product.list} element={withPermission(Permissions.MANAGE_PRODUCTS, <ProductList />)} />
|
||||||
<Route path={Paths.print.create} element={<CreateSection />} />
|
<Route path={Paths.product.create} element={withPermission(Permissions.MANAGE_PRODUCTS, <CreateProduct />)} />
|
||||||
<Route path={Paths.print.update + ":id"} element={<UpdateSection />} />
|
<Route path={Paths.product.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateProduct />)} />
|
||||||
<Route path={Paths.print.form + ":itemId/:orderId"} element={<PrintForm />} />
|
<Route path={Paths.product.attribute.create + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <CreateAttribute />)} />
|
||||||
|
<Route path={Paths.product.attribute.list + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <AttributeList />)} />
|
||||||
|
<Route path={Paths.product.attribute.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateAttribute />)} />
|
||||||
|
<Route path={Paths.product.attributeValue.list + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <AttributeValues />)} />
|
||||||
|
<Route path={Paths.product.category.list} element={withPermission(Permissions.MANAGE_PRODUCTS, <CategoryList />)} />
|
||||||
|
<Route path={Paths.product.category.children + ":parentId"} element={withPermission(Permissions.MANAGE_PRODUCTS, <CategoryList />)} />
|
||||||
|
<Route path={Paths.product.category.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateCategory />)} />
|
||||||
|
|
||||||
<Route path={Paths.users.list} element={<UsersList />} />
|
<Route path={Paths.formBuilder.list + ":id/:type"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderList />)} />
|
||||||
<Route path={Paths.users.create} element={<CreateUser />} />
|
<Route path={Paths.formBuilder.create + ":id/:type"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderCreate />)} />
|
||||||
<Route path={Paths.users.update + ":id"} element={<UpdateUser />} />
|
<Route path={Paths.formBuilder.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderUpdate />)} />
|
||||||
|
<Route path={Paths.formBuilder.values + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderValues />)} />
|
||||||
|
|
||||||
<Route path={Paths.admin.list} element={<AdminList />} />
|
<Route path={Paths.order.list} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrdersList />)} />
|
||||||
<Route path={Paths.admin.create} element={<CreateAdmin />} />
|
<Route path={Paths.order.details + ":id"} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderDetail />)} />
|
||||||
<Route path={Paths.admin.update + ":id"} element={<EditAdmin />} />
|
<Route path={Paths.order.create} element={withPermission(ORDER_VIEW_PERMISSIONS, <NewOrder />)} />
|
||||||
|
<Route path={Paths.order.edit + ":id"} element={withPermission(ORDER_VIEW_PERMISSIONS, <EditOrder />)} />
|
||||||
|
<Route path={Paths.convertToOrder} element={withPermission(ORDER_VIEW_PERMISSIONS, <ConvertToOrders />)} />
|
||||||
|
<Route path={Paths.orderPrint + ':id/preview'} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderPrintPreview />)} />
|
||||||
|
<Route path={Paths.orderPrint + ':id'} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderPrint />)} />
|
||||||
|
|
||||||
<Route path={Paths.role.list} element={<RoleList />} />
|
<Route path={Paths.tickets.list} element={withPermission(Permissions.MANAGE_TICKETS, <TicketList />)} />
|
||||||
<Route path={Paths.role.create} element={<CreateRole />} />
|
<Route path={Paths.tickets.detail + ":id"} element={withPermission(Permissions.MANAGE_TICKETS, <TicketDetail />)} />
|
||||||
<Route path={Paths.role.update + ":id"} element={<UpdateRole />} />
|
|
||||||
|
<Route path={Paths.print.list} element={withPermission(Permissions.VIEW_PRINT_FORM, <SectionList />)} />
|
||||||
|
<Route path={Paths.print.create} element={withPermission(Permissions.VIEW_PRINT_FORM, <CreateSection />)} />
|
||||||
|
<Route path={Paths.print.update + ":id"} element={withPermission(Permissions.VIEW_PRINT_FORM, <UpdateSection />)} />
|
||||||
|
<Route path={Paths.print.form + ":itemId/:orderId"} element={withPermission(Permissions.VIEW_PRINT_FORM, <PrintForm />)} />
|
||||||
|
|
||||||
|
<Route path={Paths.users.list} element={withPermission(Permissions.VIEW_USERS, <UsersList />)} />
|
||||||
|
<Route path={Paths.users.create} element={withPermission(Permissions.VIEW_USERS, <CreateUser />)} />
|
||||||
|
<Route path={Paths.users.update + ":id"} element={withPermission(Permissions.VIEW_USERS, <UpdateUser />)} />
|
||||||
|
|
||||||
|
<Route path={Paths.admin.list} element={withPermission(Permissions.MANAGE_ADMINS, <AdminList />)} />
|
||||||
|
<Route path={Paths.admin.create} element={withPermission(Permissions.MANAGE_ADMINS, <CreateAdmin />)} />
|
||||||
|
<Route path={Paths.admin.update + ":id"} element={withPermission(Permissions.MANAGE_ADMINS, <EditAdmin />)} />
|
||||||
|
|
||||||
|
<Route path={Paths.role.list} element={withPermission(Permissions.MANAGE_ROLES, <RoleList />)} />
|
||||||
|
<Route path={Paths.role.create} element={withPermission(Permissions.MANAGE_ROLES, <CreateRole />)} />
|
||||||
|
<Route path={Paths.role.update + ":id"} element={withPermission(Permissions.MANAGE_ROLES, <UpdateRole />)} />
|
||||||
|
|
||||||
<Route path={Paths.features.list} element={<FeaturesList />} />
|
<Route path={Paths.features.list} element={<FeaturesList />} />
|
||||||
<Route path={Paths.features.create} element={<CreateFeature />} />
|
<Route path={Paths.features.create} element={<CreateFeature />} />
|
||||||
<Route path={Paths.service.print} element={<PrintService />} />
|
<Route path={Paths.service.print} element={<PrintService />} />
|
||||||
|
|
||||||
<Route path={Paths.announcement.list} element={<AnnouncementList />} />
|
<Route path={Paths.announcement.list} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementList />)} />
|
||||||
<Route path={Paths.announcement.detail + ":id"} element={<AnnouncementUpdate />} />
|
<Route path={Paths.announcement.detail + ":id"} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementUpdate />)} />
|
||||||
<Route path={Paths.announcement.create} element={<AnnouncementCreate />} />
|
<Route path={Paths.announcement.create} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementCreate />)} />
|
||||||
|
|
||||||
<Route path={Paths.criticisms.list} element={<CriticismsList />} />
|
<Route path={Paths.criticisms.list} element={withPermission(Permissions.MANAGE_CRITICISMS, <CriticismsList />)} />
|
||||||
<Route path={Paths.criticisms.detail + ":id"} element={<CriticismsDetail />} />
|
<Route path={Paths.criticisms.detail + ":id"} element={withPermission(Permissions.MANAGE_CRITICISMS, <CriticismsDetail />)} />
|
||||||
|
|
||||||
<Route path={Paths.learning.list} element={<LearningList />} />
|
<Route path={Paths.learning.list} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningList />)} />
|
||||||
<Route path={Paths.learning.create} element={<LearningCreate />} />
|
<Route path={Paths.learning.create} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningCreate />)} />
|
||||||
<Route path={Paths.learning.category} element={<LearningCategory />} />
|
<Route path={Paths.learning.category} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningCategory />)} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { type FC, type ReactNode } from 'react'
|
||||||
|
import { Navigate } from 'react-router-dom'
|
||||||
|
import { useGetAdminMe } from '@/pages/admin/hooks/useAdminData'
|
||||||
|
import {
|
||||||
|
getDefaultAdminPath,
|
||||||
|
hasPermission,
|
||||||
|
} from '@/config/permissions'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
permission: string | string[]
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks route access when the current admin lacks the required permission(s).
|
||||||
|
* Pass an array to allow access if the user has any of them.
|
||||||
|
*/
|
||||||
|
const PermissionGuard: FC<Props> = ({ permission, children }) => {
|
||||||
|
const { data: adminMe, isLoading } = useGetAdminMe()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions =
|
||||||
|
adminMe?.data?.role?.permissions?.map((p) => p.name) ?? []
|
||||||
|
|
||||||
|
if (!hasPermission(permissions, permission)) {
|
||||||
|
return <Navigate to={getDefaultAdminPath(permissions)} replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PermissionGuard
|
||||||
@@ -66,7 +66,7 @@ const SideBar: FC = () => {
|
|||||||
title={t('sidebar.mainPage')}
|
title={t('sidebar.mainPage')}
|
||||||
isActive={isActive('/home')}
|
isActive={isActive('/home')}
|
||||||
link={Paths.home}
|
link={Paths.home}
|
||||||
activeName=''
|
activeName='dashboard'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
|
|||||||
Reference in New Issue
Block a user